Elasticsearch7.8.0


Author
|
Earl
Describe
|
该文档整理Elasticsearch7.x的原理、用法、相关配置和单机集群配置
Last Update
|
2024-5-07

很完整的课程参考文档:https://blog.csdn.net/SeniorShen/article/details/111591122,这里面的内容可以参考一下,总结的还是不错

  1. 简介

    • 互联网的数据一般分为图片、文章、视频、网站信息,一般根据数据的格式会将数据分成三个大类,结构化数据、非结构化数据、半结构化数据

    • 结构化数据使用特定的结构如二维表结构来组织和管理数据,一般这些数据间都有关系,使用关系型数据库Mysql、Oracle,通过sql语句来查询数据并使用索引对查询进行优化,这种方式方便管理和查询,但是扩展结构很难;

    • 非结构化数据无法使用二维表结构来表现数据,如服务器日志、通信记录、工作文档、报表、视频、图片等维度广、数据量大,数据存储和查询的成本很大,这种数据一般存在存入nosql数据库中,如MongoDB、Redis、DataBase,以key-value结构保存,通过key查询对应数据,相对来说比较快,这种数据一般需要专业的人员和大量的统计模型来进行处理;

    • 半结构化数据是将数据和结构混合在一起,没有明显的区分,比如xml、html,这种文档就是半结构化数据,这种数据一般也是保存在MongoDB、Redis、DataBase中,缺点是查询其中的数据内容不是很方便;ES用户解决实时准确地查询任何来源、任何格式的数据【同时包含结构化数据和非结构化数据】,然后实时地对数据进行搜索、分析和可视化的问题

    • EalsticSearch解决的就是如何准确查询结构化数据和非结构化数据的问题,开源的Elasticsearch是目前全文搜索引擎的首选。它可以快速地存储、搜索和分析海量数据。 维基百科、Stack Overflow、Github都采用它。 Elastic的底层是开源库Lucene【Lucene是使用java开发的】,Elastic是Lucene的封装,提供了REST API的操作接口,开箱即用。全文搜索又叫全站搜索,用户可以根据热门词汇查询整个网站中的热门文章并以列表的形式展现结果

    • ES请求和响应体的数据都是json串的格式,数据发送和返回都是json格式,json全称JavaScript Object Notation,表示一种特殊标记的JavaScript对象;json格式特别适合将对象用字符串进行网络传递;而一个对象无法使用网络传递,只能通过序列化后传递然后在客户端反序列化为对象,比较麻烦;json字符串就是网络传递数据的字符串符合json格式的字符串

    • 官方文档:[Elasticsearch Guide 8.13] | Elastic

    • 官方中文文档:Elasticsearch: 权威指南 | Elastic

    • ElasticSearch将Apache的Lucene封装到REST API级别,封装度非常高,不像MySQL一样需要引入配置驱动包,编写CRUD代码

 

HTTP API

以下使用Postman测试,实际生产中可以内嵌到网页中来发起请求提交和查询数据

  1. http://localhost:9200【Get】

    • 测试ES的HTTP协议的RestFul端口

  2. http://127.0.0.1:9200/shopping【PUT】

    • ES创建索引,等同于创建数据库

    • 幂等性,重复发起报错

  3. http://127.0.0.1:9200/_cat/indices?v【GET】

    • 查看ES中的所有索引

  4. http://127.0.0.1:9200/shopping【GET】

    • 查看单个索引的内容简介【不包含数据】

  5. http://127.0.0.1:9200/shopping【DELETE】

    • 删除ES中单个索引

  6. http://127.0.0.1:9200/shopping/_doc【POST】【请求体提交JSON格式数据】

    • 索引已经创建好了,接下来我们来创建文档,并添加数据,数据格式为json,这种方式生成的文档数据的标识是随机唯一标识,不指定主键ID的文档数据添加不能为PUT方式,会报错

    • http://127.0.0.1:9200/shopping/_doc/1【POST】【请求体提交JSON格式数据】,指定自定义唯一标识主键ID的文档数据添加,明确数据主键的请求方式可以为PUT,两种请求方式都可以,区别不明确主键的方式

  7. http://127.0.0.1:9200/shopping/_doc/1【GET】

    • 根据文档的唯一主键标识让ES服务器返回对应的文档数据,文档数据在_source属性中,传入什么内容,响应_source属性内容就是什么内容

  8. http://127.0.0.1:9200/shopping/_search【GET】

    • 查看单个索引下的所有文档数据,单个查看的响应结果以json数组的方式赋值给hits.hits属性

  9. http://127.0.0.1:9200/shopping/_doc/1【POST】【请求体提交JSON格式数据】

    • 全量修改,带唯一标识主键ID,会将原来数据完全覆盖

    • 请求体json

  10. http://127.0.0.1:9200/shopping/_update/1【POST】【请求体提交JSON格式数据】

    • 局部修改,在json对象的doc属性中传递需要修改的数据

    • 请求体json

  11. http://127.0.0.1:9200/shopping/_doc/1【DELETE 】

    • 删除索引下对应主键的文档数据,删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)

  12. http://127.0.0.1:9200/shopping/_search?q=category:小米【GET】

    • 查询索引shopping下的文档数据中包含属性category为小米的所有文档数据,响应也是响应所有满足条件的数据显示在hits.hits中

    • URL带参数形式查询,不善者容易搞攻击,或参数值出现中文可能乱码。避免这些情况可使用带JSON请求体请求进行查询

  13. http://127.0.0.1:9200/shopping/_search【GET】

    woc,GET请求可以带请求体

    • 查询文档中满足属性category的值为小米的请求体JSON

    • 查询对应索引下的所有文档数据的请求体JSON

    • 查询索引下所有结果文档数据并只显示文档数据指定属性的JSON

      "_source":["title"]表示只显示_source属性下的title属性

    • 查询索引下所有内容并分页显示的请求体JSON

      from是当前页的第一条数据的条数,满足(页数-1)*每页展示条数,比如下面的例子就是(2-1)*2,表示展示第二页数据,每页展示两条数据,响应索引下总的数据条数和当前页需要展示的数据

    • 同一个索引下的查询结果根据某个属性进行排序的请求体JSON

      sort.price.order表示使用文档数据的price属性来进行排序

  14. http://127.0.0.1:9200/shopping/_search【GET】【请求体JSON数据】

    对一个索引下的文档数据多条件查询

    • 多条件同时满足【交集】使用must的请求体JSON

      must相当于数据库的&&,查询文档数据中同时包含"category":"小米""price":3999.00的文档数据

    • 满足多条件中的一个【并集】使用should的请求体JSON

      should相当于数据库的||,查询文档数据中包含"category":"小米"或者"category":"华为"的文档数据

    • 满足范围查询使用filter的请求体json

      filter.range表示范围查询,price表示根据price属性进行范围查询,"gt":2000表示大于2000;即对查询结果中文档数据的price属性大于2000的数据进行展示

  15. http://127.0.0.1:9200/shopping/_search【GET】【请求体:JSON数据】

    • 全文检索的请求体JSON

      • 像搜索引擎一样,查询匹配内容输入“小华”,返回结果带回品牌有“小米”和"华为",原因是ES对match条件中的数据匹配时会将查询请求需要匹配的属性值拆分成一个单独的字符,对每个字符都进行单独匹配再汇总,"category" : "小米"只会查询到category属性下能匹配上小的文档数据和能匹配上米的文档数据,取并集进行输出;"category" : "小华"会查询匹配文档数据中能匹配小字的文档数据和能匹配上华字的文档数据并求并集返回

      • json数据

    • 完全匹配

      • 会精确地去匹配输出的查询条件,此时对match_phrase输入的属性值会去精确匹配文档数据,比如匹配"category" : "小华"就会精确地找文档中的category属性含有小华的文档数据

      • json数据

        下面的匹配条件就会返回文档数据总数为0,如果是match匹配会返回category属性含有小米和华为的文档数据的并集

    • 高亮查询

      hightlight属性指定查询数据的某个属性的属性值高亮显示的json请求体数据,就是在对应的内容两边添加html标签,每个文字都会添加这个标签,为什么不一起添加该标签?

      以下就是查询shopping索引下的category属性值匹配"为"的文档数据

      • json数据

  16. http://127.0.0.1:9200/shopping/_search【GET】【请求体:json数据】

    • 聚合查询

      允许使用者对es文档进行统计分析,类似与关系型数据库中的group by,当然还有很多其他的聚合,例如取最大值max、平均值avg等等,就是根据指定的属性值分组,对每一组的文档数量进行计数

      • 带回索引下满足条件的完整文档内容和统计数据的请求体JSON

        响应数据中仍然是hits.hits中是所有的文档数据,在aggregations.buckets中是对应的分组数据,分组只显示对应的分组属性值和相应的数量

        请求体json中的"terms"的含义是根据内部指定字段对数据进行分组,内部写分组依据的字段

      • 不带回文档数据只带回统计数据的请求体JSON

        加了size属性的属性值为0,响应的hits.hits就是空串

      • 对指定属性值求平均值的请求体

  17. http://localhost:1004/_cluster/health【GET】集群中某个节点和整个集群的状态检查

    • 响应内容

      注意,更改了ES集群中某个节点的配置文件【端口号】,需要将ES文件的数据目录删掉,否则查询集群健康状态会报错error并响应状态码503

      • "status": "green",表示集群状态正常

      • "number_of_nodes": 1,表示当前集群节点数量为1

      • "number_of_data_nodes": 1,表示当前数据节点数量为1

 

Java API

  1. 环境搭建

    • 创建maven项目并配置pom.xml

    • 对应文档数据的实体类

  2. 创建操作ES的客户端

  3. 创建索引

  4. 查询索引

  5. 删除索引

  6. 新增文档

  7. 修改文档

  8. 查询文档

  9. 删除文档

  10. 批量新增文档

  11. 批量删除文档

  12. 高级查询API

  13. 查询指定索引下的全部文档

     

 

 

RESTFUL简介

RESTful的REST是Representational State Transfer的开头,表示请求资源状态转换

  1. 简介

    • 缩写:REST【不是"rest"这个单词】

    • 外文名:Representational State Transfer,简称REST,

      • Resource:资源,即数据(这是网络的核心)

      • Representational:某种表现形式,比如用JSON,XML,JPEG等

      • State Transfer:状态变化。通过HTTP的动词(get查询、post新增、put修改、delete删除)实现

    • 中文名:表现层状态转移

    • 提出时间:2000年

    • 属性:一种软件架构风格【以Web为平台的。web服务的架构风格,前后端接口时候用到】,是一种组织Web服务的架构,不是一种技术也不是一种标准,作为一种架构,其提出了一系列架构级约束

  2. Rest架构的五条约束

    一个系统满足了上面所列出的五条约束,那么该系统就被称为是RESTful的

    • 使用客户/服务器(b/s、 c/s)模型。客户和服务器之间通过一个统一的接口来互相通讯。

    • 层次化的系统。在一个REST系统中,客户端并不会固定地与一个服务器打交道。

    • 无状态。在一个REST系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息。

    • 可缓存。REST系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能。

    • 统一的接口。一个REST系统需要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得REST系统中的各个子系统可以独自完成演化。【无论什么样的资源都应该使用相同的相同的接口对资源进行访问,使用HTTP协议提供的标准请求方式对资源进行操作;即对同一资源的访问都使用相同的URI,URI管理资源的定位;使用不同的方法来对同一个资源进行不同的操作,幂等性,后续无论多少次GET、PUT、DELETE、HEAD请求都是幂等性的,无论后续发起多少次相同的请求都不会对数据产生更多的影响,但是POST请求就不是幂等性的了】

  3. Rest架构的优点

    URI:统一资源路径,作为资源的唯一标识

    • 适合做前后端分离项目

    • 前端拿到数据只负责展示和渲染,不对数据做任何处理。后端处理数据并以JSON格式传输出去,定义这样一套统一的接口,在web,ios,android三端都可以用相同的接口,RESTFUL让后端接口实现天然的跨平台【因为不需要写三次代码,一次代码可以公用给三端;另外,修改代码只要修改一次,三端都同步访问新代码,不需要修改三次代码】

 

 

集群配置

windows环境

  1. 清空根目录下的data文件目录和logs目录下的所有文件

    • 因为以前可能使用过该软件,导致有数据

    • 直接删掉整个data文件夹

    • 保留logs目录,清空logs目录下的所有的文件

  2. config/ealsticsearch.yml对集群环境的第一个节点进行配置

    • 默认配置全是注释,集群环境下需要对以下指定选项进行配置

    • 集群名称cluster.name: my-application,多个EalsticSearch节点的集群名称必须相同

    • 当前节点的名字node.name: node-1,同一个EalsticSearch集群每个节点的名字不能重复

    • 主机名称network.host: localhost,当前节点所在的主机的IP

    • Http端口号http.port: 1001

    • TCP通信监听端口号transport.tcp.port: 9301

    • 指定当前节点是master节点和数据节点

    • 跨域配置

    • 双击/bin/elasticsearch.bat启动该节点

      启动以后能看到以下控制台日志说明启动成功,不成功可能是端口号占用,端口排查和杀掉进程查看【工具目录下的windows指南】

    • 使用PostMan发送请求http://localhost:1004/_cluster/health【GET】检查集群中某个节点和整个集群的状态

      • 请求任意一台节点的HTTP端口都行,都会显示当前节点的状态信息和集群信息

      • 响应内容

        注意,更改了ES集群中某个节点的配置文件【端口号】,需要将ES文件的数据目录删掉,否则查询集群健康状态会报错error并响应状态码503

        • "status": "green",表示集群状态正常

        • "number_of_nodes": 1,表示当前集群节点数量为1

        • "number_of_data_nodes": 1,表示当前数据节点数量为1

  3. 配置集群其他节点

    • 将第一个节点的整个ES拷贝一份新的,将data目录删掉,logs目录下的所有文件全部删掉

    • 更改config/elasticsearch.yml中的节点名称、Http端口号、TCP通信监听端口号

    • 在后续节点config/elasticsearch.yml添加以下配置

      ES的单个节点不知道其他节点的具体位置,需要使用下列配置对ES进行告知,这是ES中查找节点的模块;第一台机器【还是master机器不用加?】不需要添加该配置【注意此时第二个节点的配置中仍然还是node.master: true】,该配置是让其他节点去找第一台机器

      • discovery.seed_hosts: ["localhost:9301"]是去查找第一台节点的集群内部通信端口

      • 同样的方式启动节点2,用PostMan发送请求http://localhost:1004/_cluster/health【GET】访问集群状态

        注意一旦更改过第一个节点的配置【端口】,其他节点的data文件夹就需要删掉,否则其他节点启动也会报错,节点新建的时候拷贝的副本也需要删掉data目录,

      • 第三台机器启动的时候既可以查找第一台机器的9301端口,也可以查找第二台机器的9302端口,所以节点发现的主机端口是一个数组,由已经运行的可供当前节点发现集群的任意一台节点端口组成【?疑问,为什么不将所有节点的该属性设置为其他节点】

  1. 常见问题

    • 以上配置在一次启动以后如果发生中途master节点变更以后第二次就启动不了的情况,使用请求http://127.0.0.1:1004/_cluster/health【GET】报错如下:

      删除data目录和日志也启动不了且报错相同,这里有可能是节点状态发生变更后发生的错误,很大原因可能是删除了data目录导致启动以后找不到master节点,或者也可能添加了第三方的分析器;但是无论如何,添加下面的配置cluster.initial_master_nodes: node-1不会出问题,而且再次启动集群不需要删除data目录,master节点状态也时正常的

      • 需要在每个节点配置文件添加以下配置重启所有ES节点即可

        这种方式不会影响master节点重选宕机节点重启以后的master节点,仍然遵循下次master节点故障后再重选的原则

         

Linux环境

安装ElasticSearch

安装步骤
  1. 从地址https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-8-0选中LINUX_X86_64 sha下载指定版本的Linux版本的ES压缩包elasticsearch-7.8.0-linux-x86_64.tar.gz并上传至Linux系统

  2. 在上传目录使用命令tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz解压缩压缩包,解压后的名字较长,使用命令mv elasticsearch-7.8.0 es-7.8.0将ES的名字修改为es-7.8.0

  3. 使用root用户创建新用户来使用es

    • 出于安全问题,ES不允许root用户直接运行,需要在root用户中创建新的用户

    • 使用命令useradd es为linux系统创建es用户

    • 使用命令passwd es在弹窗界面为es用户设置密码

    • 如果用户创建错误可以使用命令userdel -r es删除用户es

    • 使用命令chown -R es:es /opt/elasticsearch/es-7.8.0设置es用户为es解压文件的所有者

  4. 修改根目录下的配置文件/config/elasticsearch.yml,添加以下配置

  5. 修改Linux系统配置文件

    ES生成的数据和文件比较多,生成文件时使用系统默认配置可能会出一些问题,需要对系统的配置进行修改

    • 使用vim /etc/security/limits.conf修改文件/etc/security/limits.conf,在文件末尾添加每个进程可以打开的文件数的限制的以下配置

    • 使用命令vim /etc/security/limits.d/20-nproc.conf修改系统配置文件,在文件末尾添加以下配置

    • 使用命令vim /etc/sysctl.conf在文件末尾追加配置一个进程可以拥有的虚拟内存的数量

    • 使用命令sysctl -p重新加载系统配置

  6. 启动ES

    • 不能直接进入es根目录使用命令bin/elasticsearch运行程序,会直接报错,因为不允许使用root用户运行程序,要使用命令su es将当前系统用户切换成es【或者自定义用户】再使用命令bin/elasticsearch来运行程序,此时如果之前没有设置chown -R es:es /opt/elasticsearch/es-7.8.0用户权限,此时启动创建文件会出现问题

安装成功测试
  1. 正常启动的效果和windows是一样的,只要出现控制台日志[2024-04-13T17:59:19,809][INFO ][o.e.n.Node] [node-1] node name [node-1], node ID [7aDVXWxuRMirgLOTBBgUPw], cluster name [my-application]没有报错就是正常启动了,更准确的测试是像服务器发送请求http://192.168.200.136:9200/_cluster/health查询节点状态并响应如下内容

    注意这一步发送请求需要关闭防火墙或者放开对应的端口

 

集群部署

  1. 从地址https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-8-0选中LINUX_X86_64 sha下载指定版本的Linux版本的ES压缩包elasticsearch-7.8.0-linux-x86_64.tar.gz并上传至Linux系统

  2. 在上传目录使用命令tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz解压缩压缩包,解压后的名字较长,使用命令mv elasticsearch-7.8.0 es-7.8.0-cluster将ES的名字修改为es-7.8.0-cluster

  3. 在虚拟机ip分别为131和135的机器上再重复解压安装一遍,实际上可以使用命令进行其他机器的文件分发,这个在尚硅谷的Hadoop课程中有讲文件分发,后续补充,这里先试用手动解压的方式安装

  4. 为所有es创建操作用户,赋予操作用户对应文件目录的权限,自定义三台主机的主机名

    自定义主机名的方法见本文档命令大全中系统操作中的主机名,三台机器136、131、135对应的主机名分别为elasticsearch1、nginx1和elasticsearch3

  5. 修改136机器上的配置文件/opt/elasticsearch/es-7.8.0-cluster/config/elasticsearch.yml

    • 136的配置文件

      初始默认配置文件全是注释,直接在文件末尾添加以下配置即可

    • 131的配置文件

      相比于136只是修改了节点名称和主机名称

    • 135的配置

      同样相比于136只是修改了节点名称和主机名称

  6. 每台主机都修改对应的系统配置文件

    ES生成的数据和文件比较多,生成文件时使用系统默认配置可能会出一些问题,需要对系统的配置进行修改

    • 使用vim /etc/security/limits.conf修改文件/etc/security/limits.conf,在文件末尾添加每个进程可以打开的文件数的限制的以下配置

    • 使用命令vim /etc/security/limits.d/20-nproc.conf修改系统配置文件,在文件末尾添加以下配置

    • 使用命令vim /etc/sysctl.conf在文件末尾追加配置一个进程可以拥有的虚拟内存的数量

    • 使用命令sysctl -p重新加载系统配置

  7. 修改每个节点所在主机的/etc/hosts文件

    这个必须把集群节点所在的所有主机名和ip的映射关系在每一台主机上都要全部写上,如果不写当前主机和对应的ip映射关系,ES节点中network.host写当前主机的主机名非master的ES节点会直接启动报错,提示以下信息【以前的其他软件集群部署都是在所有节点主机的/etc/hosts文件中写上包括本机在内的完整的节点映射信息】

    • /etc/hosts文件示例

      即每个节点所在主机的hosts中都要配置完整集群节点的主机名IP映射,如果当前主机没有配置,非masterES中的network.host使用当前主机的主机名会直接导致ES启动报错,此时把network.host配置成0.0.0.0能够正常启动,但是不建议这么做,此外一定不能在/etc/hosts中将主机名配置成127.0.0.1的映射关系,会直接导致包括master节点在内的ES服务器无法被访问【连HTTP端口也无法访问】

  8. 切换成对应机器分别切到操作用户按顺序启动三个节点,使用PostMan发送请求http://192.168.200.136:9200/_cat/nodes【GET】查看节点所在集群的所有节点信息

    • 当只有136节点启动时的响应结果

    • 当136和131都启动时的响应结果

    • 当136、131和135都启动时的响应结果

       

核心概念

索引

  1. 概念

    • 索引就是一个拥有几分相似特征的文档的集合 ,理解成比如订单数据有共同点,这个共同点就可以作为订单数据整体的索引,索引可以更形象的类比为新华字典的索引目录,按首字母排序,同一个首字母下又按照第二个、第三个首字母依次排序,这样的设计是为了加快检索速度

  2. 特征

    • 一个索引由一个名字来标识(该名字必须全部是小写字母),当我们要对这个索引中的文档进行索引、搜索、更新和删除的时 候,都要使用到这个名字

    • 在一个集群中,可以定义任意多的索引,能搜索到的数据都必须索引

    • 一个索引可以被分成多片,也可以被复制0次和多次,一旦被复制,索引就有了主分片和复制分片,分片和复制数量可以在索引创建时进行指定,索引创建后可以动态地改变索引复制的数量,但是不能改变索引分片的数量;默认情况下每个索引有一个主分片和一个复制分片,即当集群至少有两个节点时,索引会默认有一个主分片和一个完全拷贝的复制分片

 

类型

  1. 概念

    • ES早期对应关系型数据库,把索引当成数据库、把类型当成表、把文档当成数据行;后来发现索引就可以直接关联数据,在索引和数据之间再添加一个类型来关联数据显得画蛇添足,在7.x的版本以后就完全抛弃了类型的概念

    • 早期的思路是在一个索引下定义一个或多个类型,一个类型是索引的一个逻辑上的分支,语义由用户自行定义,通常会将由一组共同字段的文档定义为一个类型

 

文档

  1. 概念

    • 文档就是一条数据,一个文档是一个可以被索引的基础信息单元,保存一条数据就是保存一个文档

  2. 特征

    • 文档以JSON的格式来表示,JSON是一种通用的互联网数据交互格式,有很多第三方类库都能解析JSON字符串

    • 在一个索引或者类型中可以存储任意数量的文档

 

字段

  1. 概念

    • JSON格式文档中的属性就是字段,对应数据库表中字段,对文档数据可以根据不同属性进行分类标识

 

映射

  1. 概念

    • 类比于MySQL,表结构信息规定了一张表有哪些字段,字段类型、长度范围、默认值、是否可以为null,索引也会有类似于表结构的概念,比如规定哪些字段可以被查询,哪些字段可以进行分词操作,都有专门的设定;该设定就是映射

    • 即映射是ES中数据的使用规则设置,按最优规则处理数据会极大地提高处理数据的性能

 

分片

  1. 概念

    • 一个索引可以存储超出单个节点硬件限制的大量文档数据,比如一个含有十亿文档数据的索引可能占据1TB的磁盘空间,但是任何一个节点都没有这么大的磁盘空间;或者数据量太大,单个节点处理搜索请求响应太慢;ES针对该问题,提供将索引划分成多份的能力,每一份就是一个分片;比如把用户索引下的文档数据按照性别拆分成两个分片,查询用户数据男性去一个分片查,女性去另一个分片查

    • 创建索引时可以指定该索引想要分片的数量,每个分片本身就是一个功能完善且独立的索引,该索引可以被放置到集群中的任何节点上

  2. 特征

    • 索引是分片的集合,当ES在索引中搜索的时候,会发送查询到每一个属于索引的分片上,合并每个分片的查询结果到一个全局的结果集

  3. 意义

    • 分片很重要,一方面允许用户水平切割/扩展用户的内容容量,另一方面允许用户在分片上进行分布式的并行的操作,从而提高查询性能和数据的吞吐量

 

副本

  1. 概念

    • 在网络或者云环境中,由于网络或者节点宕机导致或者任何原因导致的查询失败随时都可能发生,需要实现一种故障转移机制,让故障发生时仍然正常运行或者合理服务降级,ES允许创建分片的一份或者多份拷贝,这个拷贝叫复制分片或者副本

  2. 意义

    • 复制分片也是一个重要的概念,一方面在节点/分片查询失败的情况下提供高可用性

      • 🔎这点要求复制分片不会与原/主要分片置于同一个节点上

    • 另一方面也能扩展系统的搜索量/吞吐量,因为搜索可以在所有的副本上同时进行

 

 

分配

  1. 概念

    • 分配是将分片分配给某个节点的过程,包括分配主分片和复制分片,如果分配的是复制分片,该过程还有从主分片复制数据的过程,该过程是由master节点完成的

 

ES集群原理

系统架构

  1. 概念

    • ES集群是由一个或者多个拥有相同cluster.name配置的ES节点组成, 它们共同承担数据和负载的压力。

      • 🔎当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

    • 主节点通过选举节点产生,负责管理集群范围内的所有变更,例如增加、删除索引,增加、删除节点等。

      • 🔎主节点不涉及文档级别的变更和搜索等操作,即使当集群只拥有一个主节点的情况下,流量的增加也不会因为只有一个主节点而因此成为系统的瓶颈。 任何节点都可以成为主节点。

    • 用户请求可以发送到包括主节点在内的集群中的任意一个节点,任意一个节点都能将用户请求转发到存储所需文档的节点,并收集从各个包含所需文档的节点收集回数据最终返回给客户端

  2. 架构图

    • 📌:Master节点上也有分片,为什么上面说master机器不涉及文档级别的变更和搜索

    • P0、P1、P2表示每个节点上的分片,R0、R1、R2表示每个分片对应的副本,分片和对应副本不能放在同一个节点上

    • 每个分片底层是一个Lucene Index,Lucene是全文检索引擎,每个分片都是一个检索引擎,查询的时候会自动将满足查询条件的多个分片数据合并在一起进行返回

      • 🔎因此文档数据的查询相对复杂一些,文档数据的新增的过程会相对更简单​

     

单节点集群

  1. 业务需求

    • 📜:​在只包含一个空间节点的集群内创建名为users的索引,分配三个主分片和一个副本

      • 🔎:​每个主分片拥有一个副本分片

    • 💡:​启动windows集群中的一个ES节点,向HTTP端口发送请求添加索引并设置映射关系

  2. 业务实现

    • 启动HTTP端口为1004的ES节点

      • 🔎:ES集群非常地坑,如果此前是三个节点的集群,此时只启动一个节点,即使是master节点也需要删除data目录,否则ES无法被访问,使用master节点也会提示master节点找不到,解决办法仅需删除data目录

    • 使用POSTMAN向节点发送请求http://127.0.0.1:1004/users【PUT】

      • 🔎:users是索引名

      • 请求体JSON

        • 🔎:设置映射参数,"number_of_shards" : 3,是设置users索引有三块分片,"number_of_replicas" : 1是复制一次

  3. 特点

    • 当集群是一个单节点集群时【只有一个master节点】,一个索引的三个主分片都会被分配在该一个节点上

    • 通过浏览器插件elasticsearch-head可以查看集群的状态,将插件压缩包elasticsearch-head-chrome-plugin.rar解压缩并安装到浏览器上,点开该插件输入ES的端口号点击连接

      • 集群健康值:yellow(3 of 6)表示当前集群的全部主分片都正常运行,但是副本都不正常

      • 以下三个副本都处于标灰状态,且都提示为Unassigned,即未分配状态;同一个节点既保存原始数据又保存副本是没有意义的,因为该节点发生硬件故障时,副本也是没有办法使用的;所以该状态也表示集群正常运行,但是应该故障时有丢失数据的风险

  4. 单点故障问题

    • 集群中只有一个master节点运行时,因为没有冗余节点,一旦发生故障,数据就会有丢失的风险,此时需要再启动一个节点来做冗余

    • 启动第二个节点加入到集群,通过插件发现三个副本分片都被分配在新加节点,所有新增的被索引的文档都会保存在主分片上,然后被并行地复制到对应的副本分片

      • 📌:注意此时直接启动第二个节点会直接抛异常,因为此前的集群状态是3个节点

      • 🔑:此时需要删除第二个节点的data目录,第一个节点启动时删除data目录即可,此时第一个节点不需要做任何操作

    • 集群状态

      • 第二个节点加入集群后,3个副本分片自动分配到该节点,星号标记的节点是master节点

      • 绿色文字集群健康值: green (6 of 6)表示所有分片都正常运行,my-application是集群名字

      • 🔎:特别注意这里的3个主分片都位于同一个节点【并没有详细说明主分片在节点上的分布行为,关注一下,目前没有任何状态表明master节点上的全是主分片,课程提了一嘴主分片的边框是粗边框,这意味这三个主分片位于同一个master节点】,具体地对文档数据操作是优先使用主分片数据还是同时可以使用复制分片上的数据暂时还没有明确

 

集群水平扩容

  1. 节点水平扩容

    • 双节点集群的默认行为是主分片位于master节点,复制分片位于第二个节点,当启动第三个节点加入集群,ES会为了分散负载主动对分片进行重新分配

      • 📌:第三个节点的启动同样需要删除data目录

      • 🔎:分配遵从两个原则,一是主分片和副本不能位于同一个节点,二是分片的分布应该尽量均匀,但是分片的分配还是ES集群自动决断的

    • 扩容后集群状态

      • 📌:感觉这不是很好的分配策略,为什么不是三个节点上都各分配一个主分片和一个复制分片,这不是更加合理吗?如果副本分片也可以同时进行使用,那么从节点上的分片数量上来看这种分配也合理,每个节点的硬件资源【CPU、RAM、I/O】被更少的分片共享,系统的性能会得到提升

      • 🔎:而且和课程的不一样,课程是主分片2放在了新增节点,实际测试是主分片1放在了新增节点

    • 副本分片可以处理数据

      • 课程后续提到,读操作的搜索和返回数据可以同时被主分片或副本分片处理,所以当你拥有越多的副本分片时,系统也将拥有越高的吞吐量,所以分片分配只需要保证主分片和复制分片不在同一个节点上,所有分片的平均分片数量保持均匀即可【增加副本只能增加读的性能,写的能力主要还是基于主分片】

  2. 扩充分片数量

    • 当分片数量小于节点数时,意味着肯定有节点无法被分配分片,这种情况是不被允许的,在索引创建的时候分片数量就已经确定了,此时在不添加索引的情况下,分片数量是固定的,添加新的节点会遇到困难

    • 因为副本分片也可以同时和主分片一样用于查询操作,所以可以通过增加复制次数达到增加分片数量的目的,此时就可以继续添加新的节点来提高系统的吞吐量

    • 发送请求http://127.0.0.1:1004/users/_settings【PUT】来重新设置主分片被复制的次数

      • 请求体JSON

    • 集群状态

 

应对集群故障

  1. 当master宕机后的集群状态

    • 此时master节点迁移到第二个节点上,主分片也被重新指定,相当于少了一份副本分片

  2. 重启第一个节点后的集群状态

    • 第一个节点需要配置集群内其他节点的内部通信地址discovery.seed_hosts: ["localhost:9303","localhost:9302"]才能在重启以后自动发现并加入集群,注意yml的冒号后面必须有空格,否则启动报错快速失败

    • 原来的master节点被永久的重新选举,宕机的节点恢复后以全新的节点加入集群,master节点只有再次宕机后才会重新进行选举,同时主分片只有所在节点宕机后才会重新指定

 

路由计算

  1. 保存数据时的路由计算

    • 当用户需要插入一条数据如zhangsan,需要先将文档数据写入对应索引下的其中一个主分片,然后再拷贝到对应的复制分片,ES集群是如何决断将该文档数据路由到哪一个主分片呢,ES集群根据自己的内部规则对文档数据进行主分片路由,这个规则就叫路由计算

    • 路由计算的规则为hash(文档数据id)%主分片数量=【0,1,2】

      • shard表示某一个分片、number_of_primary_shards表示主分片数量

      • routing表示路由参数,通过该文档可以自定义文档到分片的映射,通过自定义该路由参数可以保证相关文档如同属于一个人的文档数据都存储到一个分片中,默认是文档数据的主键id

      shard=hash(routing)%number_of_primary_shards

       

    • 示意图

  2. 查询数据时的路由计算

    • 分片控制:用户可以访问任何一个节点来获取数据,因为存放规则是固定的,用户访问的这个节点称为协调节点

    • 一般情况下系统的分片控制规则是轮询,分片控制意思是用户访问节点的负载均衡策略,因为任意一台机器都能转发请求获取到数据,通过主键id取哈希对主分片数量取模就能得到主分片位置进而转发请求获取数据,而采用轮询的策略能够避免在用户请求层面发生流量倾斜的情况

     

写数据流程

用户请求发送给集群时并不知道数据会写到哪个分片,到达集群上某个节点后经过计算才知道数据将被写入到具体哪一个主分片

  1. 新建、索引、删除文档数据的流程

    • 1️⃣:客户端发送新建、索引、删除请求到任意集群节点

      • 🔎:该节点称为协调节点

    • 2️⃣:协调节点通过路由计算将请求转换到指定主分片所在节点

    • 3️⃣:主分片将数据保存

    • 4️⃣:主分片将数据并行发送到各个副本

    • 5️⃣:副本保存后将保存结果进行反馈

      • 📌:反馈到主分片还是协调节点,暂时认为是主分片

      • 🔑:经过文档确认是反馈到主分片所在节点

    • 6️⃣:主分片将保存结果进行反馈

      • 📌:反馈给客户端还是协调节点,暂时认为是协调节点

      • 🔑:经过文档确认是反馈到协调节点

    • 7️⃣:协调节点向客户端反馈结果,客户端获取写数据反馈

  2. 设置可选请求参数

    以上的流程是完整的写数据流程,一些可选的请求参数允许用户影响该流程,比如在主分片保存数据成功后就可以开始直接请求数据,因为ES的性能已经很高了,这些设置请求参数的操作很可能以数据安全为代价提升系统性能,所以这些选项实际上很少使用

    • consistency

      • 参数值: "consistency" : ["quorum"|"one"|"all"]

      • 默认值: "consistency" : "quorum"

      • 配置说明:满足指定数量活跃可用的分片副本主分片才能执行写操作

        • 🔎"consistency" : "one":只要主分片活跃就允许执行写操作;"consistency" : "all":必须要主分片和所有复制分片都活跃才允许执行写操作;"consistency" : "quorum":规定数量的分片副本【分片副本包含了主分片和复制分片】活跃就允许执行写操作

        • 🔎:默认配置即"consistency" : "one"下,在仅仅视图写操作之前,主分片都要求必须有规定数量的活跃分片副本,才会执行写操作,这种设计是为了避免发生网络分区故障时进行写操作导致不同分区的数据不一致,规定数量的计算公式是

          int((primary+number_of_replicas)/2)+1
          • 🔎number_of_replicas 指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数,如果索引设置中规定当前索引拥有三个副本分片,规定数量为((1+2)/2)+1=3primary暂时认为是主分片数量】,此时如果只启动两个节点,处于活跃状态的分片副本只有2,达不到规定数量,此时将无法索引和删除任何文档

        • 🔎:新索引默认有一个副本分片,在默认配置下意味着为满足规定数量需要两个活跃的分片副本,显然这种默认设置会阻止用户在单一节点上做任何事情,为了避免该问题,默认配置的规定数量计算公式只会在指定number_of_replicas副本分片数大于1的情况下才会执行

    • timeout

      • 参数值: "timeout" : 100

      • 默认值: "timeout" : 60s

      • 配置说明:没有足够副本分片的情况下Elasticsearch的最长等待时间

        • 🔎:如果没有足够的副本分片,Elasticsearch会进行等待,期望更多的分片出现

        • 🔎:默认配置下Elasticsearch最长等待一分钟,可以通过设置timeout参数来使得等待更早终止,默认单位是毫秒,即"timeout" : 100表示等待时间为100ms,也可以通过"timeout" : 30s来指定最长等待时间为30秒

       

读数据流程

  1. 获取文档数据的流程

    • 1️⃣:客户端向任意一个节点发起查询请求,该节点被称为协调节点

    • 2️⃣:协调节点通过文档id计算文档数据所在的主分片和对应的全部复制分片位置

    • 3️⃣:采用轮询的负载均衡策略来轮询所有的分片副本【包含主分片和所有的副本分片】

      • 🔎:当文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片上,这种情况下副本分片可能会报告文档不存在,但是主分片仍然能成功返回文档,

    • 4️⃣:分片节点将数据返回给协调分片,协调分片将结果返回给客户端

 

更新数据流程

  1. 更新数据的流程

    • 1️⃣:客户端向任意一个协调节点发送更新请求

    • 2️⃣:协调节点根据文档数据的id计算主分片所在节点位置,将更新请求转发到主分片所在的节点

    • 3️⃣:主分片检索文档,修改文档内容,并尝试重新索引主分片中的文档;如果当前文档正在被另一个进程修改,当前进程无法拿到文档的锁,会一直重试步骤3,直到超过retry_on_conflict次后放弃

    • 4️⃣:如果主分片被成功更新,主分片会将新版本文档并行地转发到全部的副本分片,副本分片更新文档并重新建立索引

    • 5️⃣:一旦副本分片都返回成功,主分片节点会向协调节点返回更新成功,协调节点向客户端返回更新成功

  2. 要点

    • 主分片并行将更改转发到副本分片时,不会转发更新请求,而是转发完整文档的新版本

    • 这些更改会被异步地转发到副本分片,无法保证更改请求以发送的顺序到达副本分片,如果仅仅是转发更改请求,则可能导致以错误顺序应用前后两次更改,导致文档被损坏

      • 👅:转发文档的最新版可能使用乐观锁检查记录数据的版本来避免数据的更新顺序错误,这只是本人臆测,原教学文档没有提及此事

 

多文档操作流程

  1. 单个mget请求取回多个文档数据的步骤

    • 1️⃣:客户端向协调节点发送mget请求

    • 2️⃣:协调节点为涉及的每个分片构建多文档获取请求,并行地转发这些请求到主分片或者副本分片的节点上,收到这些节点的答复后,汇总这些答复构建响应并返回给客户端

      • :请求主分片和副本分片的行为没有说明

      • 👅:感觉还是像单个查询请求一样在协调节点直接轮询所有的分片副本,而不是单独将请求发送给主分片,由主分片负责后续的查询工作

  2. 单个批量请求bulk API中执行多个创建、索引、删除和更新请求

    • 1️⃣:客户端向协调节点发送bulk请求,协调节点解析请求为每个主分片所在节点创建一个批量请求,将这些批量请求并行转发到包含主分片的节点

    • 2️⃣:所有相关主分片按顺序执行每个操作,每个操作执行成功时主分片并行转发完整新文档到对应所有副本分片,然后执行下一个操作

    • 3️⃣:当所有副本分片执行成功并报告给主分片,主分片向协调节点报告执行成功,协调节点将响应收集并返回给客户端

 

分片原理

分片是Elasticsearch中的最小工作单元,对文档数据的检索和写操作都是基于分片完成的,一个索引的数据量太大以后会影响文档查询的效率,把一个大的索引拆成几个部分,每个部分叫一个分片;每个分片组合在一起就是一个完整的索引数据;写入完成以后分片会进行一个倒排索引的建立,在查询中使用倒排索引快速查询到文档数据;Elasticsearch底层是Lucene,Elasticsearch的倒排索引就是Lucene的倒排索引

倒排索引

  1. 概念

    • 传统数据库每个字段都存储单个值,但是全文检索中文档中字段的每个词都要被搜索,对数据库来说要求单个字段具有索引多值的能力,满足一个字段多个值需求的最好的数据结构是倒排索引,也叫倒向索引,倒向索引还有对应的正向索引

      • 👅:实际上满足一个字段多个值的数据结构也包含正向索引,只是正向索引无法满足实时排名结果或者更高相关性的需求,不可能遍历每篇文档找到其中的关键词来判断是否相关,由此需要更好的倒排索引,即根据关键词来匹配更吻合的文档

    • 正向索引:搜索引擎将待搜索的文件和一个文件id对应形成k-v键值对,然后针对文档对文档中的关键词进行统计计数

      • 🔎:这种方式在搜索引擎上天文数字一般的文档数目条件下,无法满足实时返回排名结果的需求,搜索引擎会将正向索引重新构建为倒排索引

    • 倒排索引:将正向索引的文件ID对应关键词的映射转换为关键词到文件id的映射,每个关键词对应一系列的文件,这些文件中都出现该关键词

  2. 倒排索引示例

    • 📜:对以下两个文档进行倒排索引

      • The quick brown fox jumped over the lazy dog

      • Quick brown foxes leap over lazy dogs in summer

    • 💡:​一个文档的倒排索引由文档中所有不重复词的列表构成,即每个不重复词都作为关键词,含有对应关键词的文档的id作为关键词值列表的一个元素,这样的关键词叫词条或者tokens【词条是索引中最小存储和查询的单元,英文文档中一般是一个单词,中文文档中一般是一个词组】,这个过程叫做分词,分词有专门的分词器,对中文和英文的分词器的分词规则是不同的,词条的集合被称为词典,词典一般有两种数据结构,一种是B+树,还有一种是HashMap

      • 倒排索引的过程

        • 先拿着检索词条去词典中检索是否存在,如果存在再去倒排表中检索词条对应的文档id的列表,拿着文档id进行匹配并拿到相关的文档数据

      • 两个文档所有的不重复词条的排序列表【倒排表】

        • 🔎:分词也会有一定策略,根据不同的业务需求不一定提取所有的不重复词,可能只抓人名等关键词,还有可能人名按需求会继续拆分出姓和名作为额外的索引词条,而且根据字段类型分词的策略也不同,当字段类型为keyword时对应值是不能进行分词的,需要完全匹配;如果字段类型是文本,就需要按照分词器的规则进行分词

        • 🔎:此外分词还可以设置ik分词器的不同分词算法,ik_max_word【表示对文档按照最细腻度的方式进行分词】和ik_smart【表示对文档按照最粗粒度的方式进行分词】

      • 此时搜索词条如quick brown,只需要查找对应词条包含的文档,每个词条都包含的文档或者词条匹配度最高的文档就是最相关的文档,在这种简单情况下,文档1的相关度比文档2高

      • 但是这种方式也存在问题,用户可能不认为单词的大小写有区分或者用户记错了对应关键词是大写还是小写;用户也可能压根就不记得关键词是单数还是复数,又或者具有相同词根但是形式不同的词;还有可能用户要表示jump的意思,但是确记成了leap或者不是相同词根但是意思相近的其他单词;在用户非常自信的检索关键字+Quick +fox【+前缀表示文档中必须出现该词条】,此时文档1为quick fox而文档2为Quick foxes导致用户不会查询到任何结果,这是很不友好的,为此发展出了以下的标准化解决方式

        核心是文档词条和查询字符串都必须标准化为相同的格式

        • 创建倒排索引的时候将关键字即词条规范为标准模式【如将词条统一为小写,将词汇统一提取为词根的格式,将同义词统一索引为相同的单词等,可能还会将文档中的姓名按姓和名拆分】,此时的文档对应的词条排序列表如下所示

        • 此时索引中不会在出现首字母大写的单词如Quick了,当直接使用用户搜索的词条Quick来匹配索引词条也会失败,因此用户检索的词条也需要和文档数据一样使用相同的标准化规则转换为+quick +fox的形式,这样两个文档都会匹配

           

文档搜索

  1. 背景

    • 倒排索引的一个特点是被写入磁盘后是不可改变的,倒排索引的不变性会带来以下好处

      • 只要不更新索引,就不需要担心高并发多线程是多进程同时修改索引数据的问题,就不需要额外日常加锁

      • 因为磁盘的索引不会更新,索引被读入内核的文件系统缓存就会留在缓存即内存中而不需要关注索引的更新问题,此时大部分请求会直接请求内存,不会命中磁盘,会极大地提升系统性能

      • 其他和索引相关的缓存,如filter缓存,因为一个生命周期内索引数据不会发生变化,这些缓存数据也不会发生变化

      • 写入单个大的倒排索引允许数据被压缩,减少磁盘I/O和内存的使用量

    • 早期的全文检索会为整个文档建立很大的一个倒排索引并写入磁盘,直到新的索引被创建,旧的索引会被直接替换,即写入磁盘的倒排索引不会发生改变

      • 这种方式虽然有索引不变性带来的好处,但是一旦需要添加一个新的可被索引和搜索的文档,需要重建整个倒排索引,这对索引可被更新的频率有非常大的限制,也因此对索引的数据量造成很大地限制

  2. 动态更新索引

    • 要保证倒排索引的不变性还要实现倒排索引的更新,解决办法是将最新时间的写操作全部写入一个新的倒排索引中,用来补充最近的修改,Lucene这个java库引入按段搜索的概念,每一段都是一个倒排索引,整个索引就是所有段的集合,将所有已知段列举在一个称为提交点的文件中

    • 按段搜索流程

      • 1️⃣:新文档被搜集并建立索引到内存索引缓存中

      • 2️⃣:缓存不时被提交,提交时一个新的段即一个追加的倒排索引被写入磁盘,一个包含新段名字的提交点被写入磁盘,所有文件系统缓存中等待的写入都被刷新到磁盘

      • 3️⃣:新的段开启,其中包含的文档也可以被搜索

      • 4️⃣:内存缓存被清空,等待接收新的文档

  3. 搜索流程

    • 当一个查询被触发,所有已知的段按顺序被查询,此项统计对所有段的结果进行聚合

    • 段是不可改变的,既不能删除旧的段中的文档,也不能修改更新旧段中的文档,取而代之的是在每个提交点包含一个.del后缀文件,该文件中包含被删除文档的段信息,即一个文档被删除仅仅只是在.del文件中被标记删除,实际查询时被删除的文档仍然会被检索匹配到,但是会在最终结果被返回前从结果集中被剔除,文档更新也是标记删除旧文档,新版本文档被索引到一个新段中,两个版本的文档都可能被一个查询匹配到,但是旧版本文档在结果集返回以前会从结果集中被剔除

    • 此外段还有合并的概念,当多个段进行合并的时候,会将标记删除的索引和文档真正地进行物理删除,避免文档数据的过度冗余存储

 

近实时搜索

  1. 不带文件系统缓存的写操作流程

    之前讲过了,大致总结

    • 客户端请求打到协调节点,协调节点计算主分片所在节点位置,转发写请求到主分片,主分片写操作结束转发完整更新后文档到所有的复制分片,每个复制分片写成功响应成功信息给主分片,主分片收到所有响应成功信息后将写入成功的信息响应给协调节点,协调节点响应成功信息给客户端

    • 此时,系统写入文档的延时是主分片写入延时+并行写入副本的最大延迟

      • 🔎:副本分片越多,数据越安全,但是可能发生的最大延迟事件可能更长【可能网络带宽资源占用越多吧】

    • 此时写入是在内存中创建索引,将索引加入内存的段中,只有段被写入磁盘以后文档数据才能供用户查询【那文档第80页上面为什么说内存中的新的段被开启,让它包含的文档可见以被搜索】,段被写入磁盘的过程被称为flush

      • :啊?这里说这个新的段被flush到磁盘以后才会返回保存成功给客户端,那不是每个写操作都要创建一个新段并写入磁盘?

      • 🔑:这里是老师为了引入后续的近实时搜索进一步优化引出的内容,即这不是最终形态,因为每次对文档进行写操作都要将段写入磁盘文档才能被索引,

      • 👅:这里感觉应该理解成文档需要被存储到磁盘上,同时建立文档索引,此时才能通过索引找到文档的位置,从而将文档从磁盘响应给用户

    • 这种方式存在问题,通过这种方式每次添加文档都要将段通过fsync函数将缓冲区的数据立即写入磁盘,通过这样来保证断电的情况下尽可能不丢失数据,但是fsync操作的代价很大,每次索引一个文档都去执行一次会造成很大的性能问题

      • 🔎:此时,发展出一种在Elasticsearch和磁盘件使用文件系统缓存来解决使尚未刷新到磁盘的文档能被搜索以及拉长新的段被fsync到磁盘的时间间隔,同时还要能保证数据的安全性,在即使突然断电的情况下也能尽可能地保全文档数据

  2. 带文件系统缓存的写操作流程

    • 1️⃣:内存索引缓冲区中的文档被写入到一个新的段中,然后这个新段会先写入文件系统缓存,稍后再被刷新到磁盘中

      • 🔎:写入文件系统缓存的代价比刷新到磁盘的代价低,且文件系统缓存中的文件可以向磁盘上的文件一样被打开和读取,通过文件系统缓存就可以使新段包含的文档在未进行一次完整提交时便对搜索可见

      • 🔎:这种将文档写入新段和文件系统缓存并打开一个新段的过程叫refresh,每个分片每秒会自动刷新一次,这也是Elasticsearch近实时搜索的含义,指的是对文档的写操作【新建、索引、删除文档数据】后文档并不是立即对搜索可见,但是会在一秒内变得可见

      • 🔎:当索引一个文档后很快地尝试搜索该文档,结果发现搜索不到,此时的解决办法是通过URI:/users/_refresh调用refresh API来执行一次手动将新段刷新到文件系统缓存【users是索引】,可以在测试的时候使用手动刷新,但不要在生产环境每次索引一个文档就去手动刷新,因为尽管刷新比提交少了很多操作,但是还是有性能开销

      • 🔎:并不是所有的情况都需要每秒刷新,使用Elasticsearch索引大量日志文件,此时应该重点优化索引速度,而不是近实时搜索,可以通过设置参数refresh_interval来降低新段刷新的频率,比如设置成30s刷新一次【理解成30s做一次检查,新段有写入数据就将新段写入文件系统缓存】,因为降低刷新频率能在一个新段上建立更大的倒排索引,而且过程中省去很多刷新开销,在生产环境建立一个大的新索引时也可以通过设置"refresh_interval": -1来关闭自动刷新功能

      • 🔎:设置刷新间隔

        • 设置刷新间隔为30s

        • 关闭自动刷新,发送请求/users/_settings【PUT】

        • 将刷新时间调整为默认值,发送请求/users/_settings【PUT】

 

持久化变更

  1. 概念

    • 动态更新索引,一次完整的提交会将段刷新【flush】到磁盘并将提交的段写入一个包含所有段列表的提交点,如果没有fsync将数据从文件缓存系统刷新到硬盘,无法保证数据在程序正常退出或者断电的情况下依然存在

    • 在完整提交来确保数据的安全性外,我们还需要方案来确保两次提交之间文件系统缓冲的数据不会因为意外情况而发生丢失,由此衍生出了一个名为translog的事务日志,每一次对Elasticsearch进行操作时都会进行日志记录

  2. 完整的一次提交流程

    • 1️⃣:一个文档被索引后会被添加到内存缓冲区,然后立即追加translog对应的事务日志,注意是文档先添加到内存然后再添加日志

      • 🔎:translog日志的作用是为还没有被刷新到磁盘的操作提供一个持久化记录,当ELasticsearch启动后,会从磁盘中使用最后一个提交点去恢复已知的最后一个段,并且重新执行translog中在最后一次提交后发生的变更操作,ELasticsearch在尝试恢复或者打开一个索引是,也会需要重放translog中的所有操作,日志越短,恢复越快,在重启节点或者关闭索引前执行手动flush的操作有益于重新打开或者恢复一个索引;

      • 🔎:在通过文档ID查询、删除、更新一个文档时,在尝试从相应的段中检索前会先检查translog中最近的变更来保证实时获取文档的最新版本

      • 🔎:默认情况下,translog每5秒钟就会被fsync刷新到硬盘,每次写请求【index, delete, update, bulk】完成之后主分片和复制分片也会执行fsync操作,当主分片和复制分片的translog被fsync到磁盘以后才会响应200OK给客户端 ;每次写操作请求后执行一个fsync会带来性能损失,bulk导入的fsync的性能开销相对其他较小,因为其在一次请求中平摊了大量文档的开销

      • 🔎:对于一些大容量偶尔丢失几秒数据无所谓的集群,可以考虑使用异步translog的fsync,即每次写操作以后都不进行日志的flush,只选用每5秒自动执行一次fsync,使用这种方式需要保证当集群发生崩溃时,丢失掉sync_interval时间段中的数据也无所谓,如果不能确定丢失数据的后果,最好是使用默认的参数"index.translog.durability": "request"来避免数据丢失

    • 2️⃣refresh使分片每秒被刷新一次,整个刷新过程依次为

      • 内存缓冲区文档被写入一个新的段且没有进行fsync操作

        • 👅:感觉像段不是写入内存缓冲区,而是文档写入内存中的段,内存中的段不会以新的形式写入文件系统缓存,而是像一种注册的行为,让内存中段的数据能像文件一样被打开和读取,而刷新操作就是将内存缓冲区中的文档内容刷新追加到这个内存中的段中

        • 👅:卧槽,下面段合并说自动刷新流程每秒会创建一个新的段,现在只能认为每次refrush都会创建一个新的段,每次都将新段写入系统文件缓冲,最后提交的时候提交了非常多的段

      • 被刷新的段被打开,段内文档对搜索可见

      • 内存缓冲区被清空

    • 3️⃣:更多的文档重复上述过程追加到该段中,操作不断被追加到事务日志中

    • 4️⃣:当间隔一段时间或者事务日志累计到一定程度,索引被刷新【flush】,一个新的translog被创建,一个全量提交被执行,整个过程依次为

      执行一个提交并且节点translog的行为叫做flush,分片每30min会进行一次自动刷新,或者当translog太大时也会自动进行刷新

      • 所有内存缓存区的文档被写入一个新的段

      • 内存缓冲区被清空

      • 一个提交点被写入硬盘

      • 文件系统缓存通过fsync被刷新到磁盘

      • 老的translog事务日志被删除

 

段合并

  1. 概念

    • 自动刷新流程refrush每秒都会创建一个新的段,段数目太多会导致文件句柄、内存和CPU运行周期消耗较大,此外每个搜索请求都要轮询检查每个段,段越多搜索速度就越慢,ELasticsearch通过段合并来解决这个问题,将小的段合并到大的段,将这些大的段合并到更大的段,段合并过程中将旧的已经删除的文档从文件系统中清除,在一个段中被删除的文档不会被拷贝到新的大段中

    • 在创建索引和搜索时会自动启动段合并

      • 合并过程中会选择部分大小相似的段,在后台将这些段合并到更大的段中

        • 👅:感觉像从文件系统缓冲将段合并成一个大段

      • 段合并结束老的段会被删除,新的段被刷新flush到磁盘,写入一个包含新段且排除旧的和较小的段的新提交点

      • 新的段被打开用来搜索,老的段被删除

      • 合并段需要消耗大量I/O和CPU资源,ELasticsearch在默认情况下对合并流程会进行资源限制,让搜索有足够的资源高效进行

 

 

文档分析

分析包含两个过程,将一块文本分成合适于倒排索引的独立词条,将这些词条统一为标准格式提高这些词条的可搜索性

分析器

分析器在执行分析过程时实际上封装了以下三个功能,分析器就是一个包中组合了以下三种函数的一个包装器,分析器将三种函数按照顺序执行

  1. 字符过滤器

    • 字符串按顺序通过一系列字符过滤器,HTML清除字符过滤器能用来去掉所有的HTML标签等字符,并且像把&Aacute转换为相对应的Unicode字符Á,或者将一些特殊含义的字符如&转换成and

    • 一个分析器可能有 0 个或者多个字符过滤器

  2. 分词器

    • 字符串被分词器分成单个词条,最简单的分词器遇到空格和标点时将文本拆分成词条,正则分词器根据匹配正则表达式来分割文本,关键词分词器完整输出接收到的字符串,不做任何分词

    • 一个分析器必须有且只有一个唯一的分词器

  3. Token过滤器

    • 分词器拆分出来的词条按顺序通过每个token过滤器,该过程可能会改变词条如将英文字母改成全部小写,删除a、and、the等无用词条,增加jump、leap等同义词词条

    • ES提供很多词单元过滤器,lowercase词过滤器将词条小写,stop过滤器是一个停用词过滤器【删除】,词干过滤器把单词转换成词干,ascii_folding过滤器移除变音符【把一个像très这样的词转换为tres】,ngramedge_ngram词单元过滤器可以产生适合用于部分匹配或者自动补全的词单元

 

内置分析器

Elasticsearch附带了可以直接使用的预包装的分析器,以下是最重要的几个分析器,以下演示分析器处理下列文档的效果:"Set the shape to semi-transparent by calling set_trans(5)"

  1. 标准分析器

    • 标准分析器是Elasticsearch默认使用的分析器,根据Unicode联盟定义的单词边界划分文本,删除绝大部分标点,最后将词条全部小写,该分析器是分析各种语言文本最常用的版本,产生的对应词条:

      set, the, shape, to, semi, transparent, by, calling, set_trans, 5

  2. 简单分析器

    • 在任何不是字母的地方分隔文本,并将词条小写,产生的对应词条:

      set, the, shape, to, semi, transparent, by, calling, set, trans

  3. 空格分析器

    • 在空格的地方划分文本,产生的对应词条:

      Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

  4. 语言分析器

    • 特定的语言分析器可以考虑指定语言的特点,如英语分析器会删除对相关性没有影响的英语无用词如the、and等,还可以理解英语语法规则,提取英语单词的词干【transparent、 calling 和 set_trans 已经变为词根格式】,产生的对应词条:

      set, shape, semi, transpar, call, set_tran, 5

 

分析器使用场景

  1. 一方面索引一个文档时,需要将文档的全文域使用分析器分析成词条来创建倒排索引,同时用户在全文域搜索的时候,需要将查询字符串使用相同的分析器经过相同的分析过程来保证搜索的词条格式和索引的词条格式一致,但是当用户查询一个精确值域查询的时候,不会分析查询的字符串,而是搜索指定的精确值

  2. 区分全文域和精确值域,全文域会对查询字符串和文档做相同的分析过程来保证搜索词条和索引词条格式的一致,精确值域不会分析查询字符串,会直接搜索指定的精确值

    • :精确值域使用什么分析器呢?

 

测试分析器

  1. 使用analyze API查看文本是如何被分析的

    • 使用请求http://localhost:9200/_analyze【GET】可以查看文本如何被分析

      • 请求体json

        analyzer指定分析器,text指定要分析的文本

      • 响应结果

        token是实际存储到索引中的词条,position指词条在原文本中出现的位置即从1开始的第几个词,start_offsetend_offset分别指明字符在原始包含空格字符串中的起始字节位置和字节结束位置,以空格作为一个词的结束且位置下标从0开始

         

指定分析器

Elasticsearch在文档中检测到一个新字符串域会自动设置该文档为一个全文字符串域,自动使用标准分析器对其进行分析,如果我们想自己指定适用实际数据使用的语言需要的分析方法,想要一个字符串域就是一个字符串域【不使用分析,直接索引传入的精确值】或者文档内部的状态域,需要要手动指定这些域的映射

这里没说怎么指定文档索引时使用哪种分析器,只说了如何制定测试时的分析器

  1. 标准分析器的缺点

    • ES默认的标准分析器无法识别中文中的词汇,只是简单滴将每个字拆到底作为一个词条,这种方式不符合实际的使用需求,一般使用对应ES版本的中文分词器如IK中文分词器

    • 标准分析器分析"测试单词"的分词效果

 

IK中文分词器
  1. 安装IK中文分词器

  2. 使用IK中文分词器进行测试

    • 发送请求http://localhost:9200/_analyze【GET】添加查询参数"analyzer":"ik_max_word"

      • "analyzer":"ik_max_word":会将文本做最细粒度的拆分,注意是按照词组的最细腻度,不会拆分成单个字,只有完全没有词组的情况下才会拆分成单个字

      • "analyzer":"ik_smart":会将文本做最粗粒度的拆分

    • 对应的分词测试响应效果

  3. 扩展词汇

    • 对于分词器无法识别的词汇如"弗雷尔卓德",IK分词器会尝试将每个字都分开作为单独的词条,用户可以尝试在plugins/ik/config目录下创建custom.dic文件,在文件中写入自定义词汇弗雷尔卓德,每个节点都要添加;

    • 同时打开IKAnalyzer.cfg.xml文件,将新创建的custom.dic文件通过以下配置加入到IK分析器中,重启ES服务器

      该文件已经写好模板,只需要将custom.dic添加到用户扩展字典处即可,每个节点都要添加

    • 再次发送请求http://localhost:9200/_analyze【GET】

      • 请求体json

      • 响应json

 

自定义分析器

ES中的自定义分析器是组合分析器的三大不同类型的函数,包括字符过滤器、分词器、词单元过滤器来组装出适用于自用场景的分析器

  1. 发送请求http://localhost:9200/my_index【PUT】通过创建索引my_index同时指定使用的自定义的分析器my_analyzer

    • 请求体JSON

      讲的太粗糙了,以后自己研究一下

      • 上面的"char_filter"是在其中定义到可能用到的自定义字符过滤器,这里的&_to_and是mapping类型,规定将&转换为and字符的

      • 上面的"filter"是定义可能用到的自定义词单元过滤器,这里的my_stopwords是stop类型,规定删除词条中的the和a词条

      • 下面的"analyzer"是指定自定义的分析器,分析器的名字叫"my_analyzer""type"是custom,表示自定义,"char_filter"指定分析器的所有字符过滤器数组【包括ES自身带的和用户在上面自定义的】,"tokenizer"是指定分词器,"filter"是指定分析器的所有词单元过滤器数组

  2. 索引创建后发送analyze API来测试自定义分析器

    • 发送http://127.0.0.1:9200/my_index/_analyze【GET】测试新的自定义分析器

      • 请求体JSON

      • 响应效果

         

文档处理

文档冲突

  1. 概念

    • ES中一次性读取原始文档【比如使用IndexAPI】,同时修改以后重新索引整个文档,最后无论哪个文档被索引,都会被唯一地保存在ES中,其他的文档更改将失败,这样的方式问题不大

    • 实际上Elasticsearch一般作为主关系型数据库的一个搜索数据库,将主关系型数据库的数据赋值到ES中并使其提供检索服务,比如存储了商城商品库存数量,每卖出一个商品,ES都会将库存相应地减少;但是在促销活动中,一秒可能会卖好几个商品,多个web线程并行地运行同一种商品的销售,同时读取了商品的库存,但是先完成的程序已经将库存耗光,后完成的程序并不知道这个情况,导致用户交易成功但实际上没有可以提供的商品,甚至可能导致先完成的库存数量扣减失效【比如使用下单时的商品库存而非结算时的商品库存,一般来说这种业务逻辑导致的问题都不算系统性问题,一般指瞬时速度太快,读到改的过程中其他变更已经发生,当前的改直接覆盖掉这期间发生的变更操作,用锁来控制拿到数据和更改的整个过程】,产生了数据变更发生丢失的问题

    • 数据变更越频繁,读数据和更新数据的间隙越长,数据变更丢失的可能就越大

    • 实际意思就是读数据和更新数据的时间间隔中发生数据频繁变更导致时间间隔中丢失数据更改的情况

  2. 确保更新时变更不丢失的方案

    • 悲观并发控制

      • 使用悲观锁来进行并发控制,悲观锁认为数据更改期间随时都会发生其他变更冲突,操作数据前需要拿到操作对象的锁,否则就进入阻塞等待状态,确保读到的数据不出错,修改的数据不发生变更丢失,但是这种方式效率低下,对相关数据操作无法并行进行

    • 乐观并发控制

      • 使用乐观锁进行并发控制会假定不会发生变更冲突,所有线程都可以并行地对数据进行操作,但是一旦通过一定手段发现读取数据到更改数据间隔期间数据发生变化,本次更新操作会失败,然后由程序来决定后续使用新数据来重新更新或者将相关情况报告给用户的后续行为

      • ES本身文档发生写操作的时候,新版本文档需要复制到复制分片中,这个过程也是异步和并发的,假如极短时间内发生两次写操作,后一次操作可能先到达复制分片,如果前一次操作直接将文档进行覆盖会导致后一次操作的变更丢失导致文档直接损坏【比如其他复制分片的顺序是正确的,导致复制分片上的文档数据不同】,ES使用version版本号来确保变更按照顺序正确地执行,早期的ES中如果修改请求的版本号不是当前当前的版本号,说明更改已经发生,更改请求会失败,现在新版本不支持使用version,会报错提示请求参数使用if_seq_noif_primary_term,但是可以都不写,这种情况仍然可以修改成功,但是请求中不携带版本号

      • 使用请求http://127.0.0.1:1004/shopping/_create/1001【PUT】创建文档时响应结果中会显示文档版本信息

        • 请求体Json

        • 响应结果

          • "_version"是文档数据的版本号,"_seq_no""_primary_term"是用于新版本版本号并发控制的判断依据

      • 使用请求http://127.0.0.1:1004/shopping/_update/1001【POST】对文档数据进行一次更新

        • 请求体JSON

        • 响应体json

          可以看到version字段和_seq_no字段都变了

      • 使用请求http://127.0.0.1:1004/shopping/_update/1001?version=2【POST】来指定版本号对文档数据进行更新,这种方式会直接报错,因为新版本不再支持指定version来使用乐观锁了,需要使用if_seq_noif_primary_term 【注意使用version或者if_seq_noif_primary_term 是针对更改需要使用乐观锁的情况,不加version字段也能更新,数据版本也会自增,但是请求数据没有加上文档的版本信息】

        • 请求体json

        • 响应体json

      • 使用if_seq_noif_primary_term 做版本控制,发送请求http://127.0.0.1:1004/shopping/_update/1001?if_seq_no=1&if_primary_term=1【POST】

        • 请求体json

        • 响应体json

          版本号和_seq_no都变大了,注意当文档内容与原来一样的情况下,版本号和_seq_no不会发生变化

           

    • 外部系统版本控制

      • 这种设置一般是用其他数据库作为主要数据库,用ES做数据检索,主数据库发生更改是被复制到ES,如果多个进程都对同一数据进行同步,可能就会遇到该并发更改丢失问题

      • 此时主数据库中一般有用于版本控制的字段如timestamp,在ES请求中通过增加version_type=external 和指定version请求参数来使用该版本号,版本号的数值范围必须对应Java中的Long类型正整数

      • 外部系统版本控制中ES不是检查当前文档的版本号是否和更改请求中的版本号是否相同来判断更改是否生效,而是判断当前文档的版本号是否小于更改请求的版本号,如果小就执行更改并将更改请求的版本号作为新文档的版本号,即这种方式将版本控制问题交给程序和主数据库,自己只负责更新到达的最终版本数据

      • 外部版本号不仅在索引和删除请求是可以指定,而且在创建新文档时也可以指定。

      • 使用请求http://127.0.0.1:1004/shopping/_doc/1001?version=1&version_type=external【POST】来使用外部系统的版本控制,注意这里是_doc,不再是上面的_update

        • 请求体JSON

        • 响应体json

          报错是因为请求中的版本号比ES中文档的版本号更低

        • 将version改成10以后的响应体json

          注意此时的请求version远大于ES中文档的version,更改后数据版本直接变成10

           

 

安装Kibana

免费开放用户界面,是ELK中的K,能让用户对ElasticSearch对数据进行可视化,此外还可以用于跟踪查询负载、理解请求如何流经整个应用

详细用法自己学习

  1. 安装

    • 1️⃣:下载地址:https://artifacts.elastic.co/downloads/kibana/kibana-7.8.0-windows-x86_64.zip,下载后解压

    • 2️⃣:修改config/kibana.yml文件增加如下配置,默认配置下配置文件都是注释掉的

      • 🔎:8.x版本开始不再需要索引配置,启动会报错,注意

    • 3️⃣:双击执行bin/kibana.bat文件,通过浏览器访问web可视化界面http://localhost:5601

      进去以后提示没有数据,但是实际用控制台查还是能查出数据

  2. 安装成功测试

    kibana会自动在ES服务器中创建几个索引

    • 点击控制台进入控制台,输入请求方式【全大写】和请求URI,点击运行能执行得到和使用PostMan一样请求的效果

      • 控制台代码示例

      • 响应效果

         

ES集成其他框架

SpringData

为了简化ES操作,该框架的目的主要是为了简化数据库、非关系型数据库、索引库的访问操作,更方便快捷地访问数据,并支持 map-reduce 框架和云计算数据服务,SpringData可以极大地简化JPA写法,几乎可以在不写实现的情况下实现对数据的访问和操作,除了CRUD外还包括分页和排序等常用功能

  1. 框架简介

    • 官网:https://spring.io/projects/spring-data

    • 常用功能模块

      Spring Data下有非常多的模块,以下只是常用的功能模块

      • Spring Data JDBC

      • Spring Data Redis

      • Spring Data ElasticSearch

      • Spring for Apache Hadoop

  2. Spring Data ElasticSearch

 

  1. 创建Maven项目并集成Spring Data ElasticSearch

    • 创建Maven项目并添加依赖关系

    • 配置application.yml

    • 编写主程序

    • 编写对应索引的数据实体类

      每个对象就是该索引下的一个文档

    • 编写配置类

      ElasticsearchRestTemplatespring-data-elasticsearch项目中的一个类,和其他 spring 项目中的 template 类似, 在新版的 spring-data-elasticsearch 中, ElasticsearchRestTemplate 代替了原来的 ElasticsearchTemplate ,原因是 ElasticsearchTemplate 基于 TransportClientTransportClient 即将在 8.x 以后的版本中移除。所以推荐使用 ElasticsearchRestTemplateElasticsearchRestTemplate 基 于 RestHighLevelClient 客 户 端 的 。 需要自定义配置类 , 继承AbstractElasticsearchConfiguration,并实现 elasticsearchClient()抽象方法来创建 RestHighLevelClient 对象并向容器进行注入

    • 编写Dao用户操作数据对象访问数据库

    • 索引操作API

      创建索引和删除索引

      【索引自动创建效果】

    • 文档操作API

      • 根据id对指定文档进行操作

        【批量保存文档数据效果】

      • 使用检索词条对文档进行搜索

         

SparkStreaming

为了将数据处理的结果放在ES中,这是和大数据相关的,写的东西看不懂,后面学大数据相关的内容再学习吧,Flink也是大数据的,TODO

 

TODO

 

Elasticsearch优化

硬件选择

  1. Elasticsearch的基础是Lucene,所有的索引和文档数据都存储在本地,路径可以在../config/elasticsearch.yml中进行配置

  2. ElasticSearch重度使用磁盘,磁盘能处理的吞吐量越大,节点越稳定,以下列举磁盘I/O的优化技巧

    • 使用SSD固态硬盘替代机械硬盘,ES是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高

    • 使用RAID0条带化RAID能提高磁盘I/O,提高性能的原理是将连续的数据分散到多个磁盘中存取,数据请求可以被多个磁盘并行地执行,代价是一块硬盘故障时整个磁盘就故障,不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能

    • 使用多块硬盘,并配置Elasticsearch通过多个path.data目录配置把数据条带化分配在多块硬盘上

    • 不要使用NFS、SMB/CIFS等远程挂载的存储,这会引入延迟

 

分片策略

  1. 合理设置分片数

    分片数不能无限地分配,主要有以下原因

    • 一个分片的底层就是一个Lucene索引,会消耗一定文件句柄、内存和CPU运转

    • 每个搜索请求都需要命中索引中的一个分片,当每个分片都处于不同的节点能够较好地分散查询压力,如果分片太多,多个查询都命中同一个节点,在同一个节点上竞争使用相同的资源就很糟糕了

    • 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度

    • 分片数量规定好以后无法进行修改

  2. 设置分片数量的原则

    • 每个分片占用的硬盘容量不要超过JVM的最大堆空间设置【一般是32G】,如索引总容量500G左右,则该索引的分片数量设置为大于16个

    • 同时还要考虑节点数量,一般设置分片数不超过节点数的3倍,如果分片数大大超过节点数,很可能导致一个节点上存在多个分片【多个复制分片位于同一个节点上?】,一旦该节点故障,同样可能导致数据丢失,主分片、副本和节点数之间的配置一般参考以下关系:节点数>=主分片书*(副本数+1)

  3. 推迟分片分配

    • 当节点瞬时中断发生后,ES会自动再平衡可用分片【暂时认为是自动重新分配索引分片】,这种再平衡分片的过程会带来极大的开销,默认情况下,集群会等待一分钟来观察瞬时中断节点是否会重新加入,如果此期间节点重新加入,不触发新的分片分配,重新加入的瞬时中间节点保持现有的分片数

    • 修改参数delayed_timeout可以延长从瞬时中断到触发新的分片分配的时间,可以全局配置也可以在索引级别进行修改

    • 发送请求/all/_settings【PUT】修改分片再分配时间间隔为5分钟,请求体JSON如下

 

路由选择

  1. 不带routing查询

    • 由于查询时不知道目标数据所在的具体分片,即无法通过路由公式计算出来,查询请求到达协调节点后,协调节点将查询请求分发到每个分片上,每个分片都处理查询请求并将查询结果响应给协调节点

    • 协调节点搜集每个分片上的查询结果,对查询结果进行排序,并将聚合的查询结果响应给客户

  2. 带路由查询

    • 查询文档时,ElasticSearch根据路由公式shard = hash(routing) % number_of_primary_shards计算出文档所在的主分片,路由routing的默认值是文档的id,也可以采用自定义值如用户id

    • 查询时可以直接根据routing信息定位到某个分片,不再需要查询所有分片

      讲的太垃圾,根本没说如何设置routing为自定义路由参数,如何根据自定义的routing参数分配数据到指定分片上,并由此根据数据分片特点以特定的自定义routing参数让一次查询请求集中在一个分片上来提升查询效率

 

写入速度优化

ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,我们需要根据公司要求,进行偏向性的优化,针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略

  1. 优化策略

    • 增大Translog日志的刷新时间,来降低磁盘的每秒输入输出Iops、写锁操作Writeblock【压根就没讲这玩意】

    • 增加索引的Refresh间隔,减少分片merge的次数【即小分片合并成一个大分片的操作】

    • 调整Bulk批量处理的线程池和队列,批量处理数据越多,效率越快【非常地敷衍】

    • 优化Lucene层面的索引建立过程,降低CPU和I/O的消耗

  2. 批量数据提交

    • 大量写任务可以使用Bulk来进行批量写入,默认情况下Bulk的默认设置批量处理单次提交的数据量不超过100MB,数据条数根据文档的大小和服务器性能确定,实际的单次提交数据量应该以5-15MB的步长一次增加,直到Bulk的批量写入性能没有提升时以该数据量作为单次批量处理提交的数据量大小

    • 段合并会频繁使用磁盘,磁盘的读写速度提升后,集群的整体性能也会大幅提升

  3. 合理使用合并

    • 新数据写入索引就会创建新的段,段的数量越多,消耗的文件句柄数和CPU就越多,查询效率就会下降

    • Lucene段合并的计算量非常庞大,会消耗大量的I/O,ES采用较保守的策略,让后台定期进行段合并

  4. 减少refresh的次数

    • Lucene在新增数据时,默认情况下索引的 refresh_interval 为1秒,即Lucene将待写入的数据先写入到内存中,默认1秒触发一次refresh,将内存中的数据刷新到操作系统的文件缓存系统中,如果对搜索的时效性要求不高,即新增数据不需要被近实时查询,将Refresh周期延长,例如 30 秒,减少refresh操作,但是这样同时会增加更多的Heap内存消耗

  5. 加大Flush设置

    • Flush的目的是将文件缓存系统中的段持久化到硬盘,Translog数据量到达512MB或者30min时就会触发一次Flush

    • index.translog.flush_threshold_size参数的默认值是 512MB,我们进行修改增大该参数值,但是同时增大该参数值意味着文件缓存系统需要更大的空间来存储更多的数据

  6. 减少副本的数量

    • 每个副本在新增数据的过程中都会执行分析索引和合并的过程,副本数量也会严重影响写索引的效率

    • 写索引时,写入的数据同步到副本节点,副本节点越多,写索引的效率就越低

    • 进行大批量写入操作时,可以先禁止副本复制,设 置index.number_of_replicas: 0 关闭副本,写入完成后在将副本恢复成可以复制的状态

 

内存设置

  1. 背景

    • ES默认安装后设置的内存是1GB,对于任何一个现实业务来说该内存设置都太小了,通过解压安装的ES在ES的安装文件中包含一个名为config/jvm.option的文件,在其中通过命令-Xms1g-Xmx1g来设置ES的堆内存大小,Xms表示堆内存的初始大小,Xmx表示可分配的最大内存,默认都是1GB

    • 确保初始堆内存大小Xmx和可分配的最大内存大小Xms是相同的,目的是确保Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力

  2. ES堆内存分配的原则

    ES的堆内存不是分配的越多越好,要满足以下两个原则

    • 堆内存不要超过物理内存的50%,Lucene的设计目的是将底层操作系统的数据缓存在内存中,Lucene中的段分别存储在单个文件中,这些段文件不会发生变化,缓存起来很方便,操作系统会将这些段文件缓存起来,一遍实现更快的访问,如果将堆内存设置过大,Lucene可以使用来进行段文件缓存的内存就会减少,严重降低Lucene的全文本查询性能,而且操作系统文件缓存空间太小会导致新增的数据频繁落盘,增加系统CPU和I/O资源的消耗

    • 堆内存大小最好不要超过32GB,Java中所有对象都分配在堆上,由一个Klass Pointer指针指向其类元数据,该指针子啊64位操作系统上为64位,64位操作系统上可以操作264字节的内存【远大于32G,这个可寻址的空间大的可怕,是4*232GB】;而在32位操作系统上为32位,最大寻址空间为232【4GB】,使用64位指针意味着指针本身大了浪费内存,同时更大的指针在主内存和缓存器(例如 LLC, L1 等)之间移动数据的时候,会占用更多的带宽【但是我实在不明白寻址空间为4GB为什么要分配32G的堆内存空间,而且也没说选择和操作系统位数之间的关系,难道我使用64位操作系统和使用32位操作系统分配的堆内存是相同的吗?讲的太烂了】,注意这里说的是每个节点的JVM堆内存不要超过32G,此前分片策略说的是每个节点上的每个分片占用的硬盘容量不要超过对应节点的堆内存空间32G,这个32G是人为一般设置,默认设置是1G,太小了

    • 一般的配置策略是采用31G配置,即修改默认配置为-Xms31g-Xmx31g

 

重要参数配置

  1. cluster.name

    • 参数值: cluster.name: elasticsearch

    • 默认值: elasticsearch

    • 作用域: ES集群

    • 配置说明:配置ES的集群名称,默认名称为elasticsearch,ES会自动发现同一网段下集群名称相同的节点

    • 配置实例

  2. node.name

    • 参数值: node.name: node-1

    • 默认值:

    • 作用域: ES节点

    • 配置说明:配置集群中的节点名,同一个集群中节点名不能重复,节点名一旦设置就不能再改变,也可以通过设置变量的方式设置成节点所在的主机名称,如node.name: ${HOSTNAME}

    • 配置实例

  3. node.master

    • 参数值: node.master: true

    • 默认值: true

    • 作用域: ES集群

    • 配置说明:指定当前节点是否有资格被选举称为master节点,设置为true表示该节点有资格成为master节点,是否成为master节点,需要通过选举产生

    • 配置实例

  4. node.data

    • 参数值: node.data: true

    • 默认值: true

    • 配置说明:指定当前节点是否存储索引数据,也即当前节点是否data节点,数据的增删改查都是在data节点完成的

    • 配置实例

  5. index.number_of_shards

    • 参数值: index.number_of_shards: 1

    • 默认值: 1

    • 配置说明:设置索引主分片个数,默认是一个主分片,也可以在创建索引时来设置索引的主分片个数,具体值要根据数据量的大小来确定,数据量不大的情况下设置为1时的效率最高

    • 配置实例

  6. index.number_of_replicas

    • 参数值: index.number_of_replicas: 1

    • 默认值: 1

    • 配置说明:设置默认的索引副本个数,默认为1个副本,副本数越多,集群的可用性越高,但是写索引时需要同步的数据越多

    • 配置实例

  7. transport.tcp.compress

    • 参数值: transport.tcp.compress: true

    • 默认值: false

    • 配置说明:设置在节点间传输数据时是否对数据进行压缩,默认为false,不进行压缩

    • 配置实例

  8. discovery.zen.minimum_master_nodes

    • 参数值: discovery.zen.minimum_master_nodes: 1

    • 默认值: 1

    • 配置说明:设置在选举master节点时需要参与的最小的候选主节点数,默认为1,

      • 🔎:如果使用默认值1,当网络不稳定时可能发生脑裂现象,即由于网络故障,master节点并没有宕机,但是集群选举出了另一个master节点,产生两个master节点的情况

      • 🔎:合理的数量为(master_eligible_nodes/2)+1,其中master_eligible_nodes表示集群中的候选主节点数

    • 配置实例

  9. discovery.zen.ping.timeout

    • 参数值: discovery.zen.ping.timeout: 3s

    • 默认值: 3s

    • 配置说明:设置集群中自动发现其他节点时Ping连接的超时时间,默认为3秒,较差的网络该值需要设置得大一些,防止因误判节点的存活状态从而导致分片的转移

    • 配置实例

     

ES项目知识

ES常用WEB API

节点信息相关

  1. 【GET】http://192.168.56.10:9200/

    • 请求体:无

    • 功能:测试ES的安装运行是否正常

    • 响应内容

  2. 【GET】http://192.168.56.10/_cat/nodes

    • 请求体:无

    • 功能:查看当前ES集群的节点信息

    • 响应内容

      • 响应当前集群下的所有节点信息,当前单节点模式启动,所以只有一个节点,这个最后的ecb880026b14就是上面URL响应结果中的节点名称

      • 星号表示当前节点是一个主节点

    • 补充说明:_cat下应该有很多的相关API,单纯的http://192.168.56.10/_cat响应结果如下,返回_cat后能跟所有子uri

  3. 【GET】http://192.168.56.10:9200/_cat/health

    • 请求体:无

    • 功能:查看ES集群的所有节点健康信息

    • 响应内容

      • green表示当前节点健康,后面的数字是集群分片信息

  4. 【GET】http://192.168.56.10:9200/_cat/master

    • 请求体:无

    • 功能:查看主节点信息

    • 响应内容

      • hcAGB9fFT0uRZ2xR36VZlA是主节点的唯一编号,ecb880026b14是主节点的名称,127.0.0.1是主节点地址

索引文档CURD相关

  1. 【GET】http://192.168.56.10:9200/_cat/indices

    • 请求体:无

    • 功能:查看ES集群中的所有索引,相当于查看查看mysql中的所有数据库

    • 响应内容

      • 目前还没有向ES中添加索引,这些索引都是kibana相关的一些配置信息,由kibana在ES中创建的

  2. 【PUT】http://192.168.56.10:9200/customer/external/1

    • 请求体

      • 请求体json就是文档数据

    • 功能:向ES服务器索引一个文档,customer是文档的索引,external是文档类型,值得注意的是在ES8中已经废除了文档类型的概念,可以直接以【PUT】http://192.168.56.10:9200/customer/1来索引文档,PUT带id的索引文档操作,PUT请求方式的索引文档必须携带id,不携带id会直接报错,多次执行是更新操作,文档数据的版本号会累加,一般都将PUT方式的索引文档用来做更新操作

    • 响应内容

      • 响应数据中以_开头的称为元数据,反应一个文档的基本信息,_index表示当前文案数据在哪一个索引下,_type表示当前文档所在的类型,_id是文档数据对应的id,_version是文档数据的版本,result是本次操作的结果,_shards是分片的相关信息

    • 补充说明

      • 再次发送该请求响应内容中的result会变成updated,且版本号自动发生了累加

      • 这种方式是全量更新,即请求体文档数据直接覆盖原文档数据内容

  3. 【POST】http://192.168.56.10/customer/external

    • 请求体

      • 请求体json就是文档数据

    • 功能:像ES服务器索引一个文档,不指定id的情况下会自动生成唯一id,多次放松请求,每次响应都是created操作,且都会响应不同的唯一id,版本号不发生变化;携带id,第一次请求是created操作,此后多次发送相同请求,会显示是updated操作,且文旦数据的id唯一,版本号会相应累加;即带id和PUT方式的功能是完全相同的,不带id自动生成id并且每次请求都是全新的新增文档操作

    • 响应内容

      【不携带id】

      【不携带id多次发送效果】

      【携带id】

      【携带id多次发送效果】

    • 补充说明

      • 这种方式是全量更新,即请求体文档数据直接覆盖原文档数据内容

  4. 【GET】http://192.168.56.10:9200/customer/external/1

    • 请求体:无

    • 功能:通过指定索引分类和id检索指定文档数据

    • 响应内容

      • _seq_no_primary_term是做乐观锁操作的,数据发生改动,序列号_seq_no就会往上加,分片发生变化如集群重启或者主分片重新选举,_primary_term也会发生相应的变化;老版本做乐观锁用的是version,新版本禁止使用version而在请求参数中带这两个参数来替代了;不过对于版本控制又外部逻辑处理的时候还是可以使用version

      • found表示对应的文档数据被找到

      • _source表示文档数据的具体内容

  5. 【PUT】http://192.168.56.10:9200/customer/external/1?if_seq_no=4&if_primary_term=1

    • 请求体

    • 功能:当文档数据的_seq_no_primary_term和请求参数的对应参数值相同时执行更新操作,即使用乐观锁来做文档数据的并发操作控制

    • 响应内容

    • 补充说明

      • 注意文档索引过程中使用了类型,更新文档时的URI中也必须使用类型,否则请求会直接报错

  6. 【POST】http://192.168.56.10:9200/customer/external/1/_update

    • 请求体

    • 功能:根据文档数据的索引、类型、文档id和文档内容对文档数据进行更新,如果文档数据和ES服务器中的文档数据内容相同,多次操作ES服务器中的数据不会发生任何变化,连数据的版本号都不会发生变化,在响应内容的result为noop,表示什么都不做,然而不带_update的更新操作不会检查原文档数据是否和需要更新后的文档数据是否一致;同时注意使用_update进行更新,更新内容要放在请求体的doc属性中

    • 响应内容

      【多次操作单文档数据不变的响应】

    • 补充说明

      • _update的更新请求只能是POST请求方式,不能使用PUT请求方式,且带_update的POST请求是局部更新,即文档数据不会直接全部覆盖,有对应属性的数据相应修改,没有对应的属性就保留原文档数据,新增没有的属性和相应的数据;但是注意不带_update的上述两种PUT和POST方式的更新都是全量更新,即直接用请求体的数据直接将原文档数据全部直接覆盖

  7. 【DELETE】http://192.168.56.10:9200/customer/external/1

    • 请求体:无

    • 功能:根据索引、类型和文档id删除指定文档数据

    • 响应内容

  8. 【DELETE】http://192.168.56.10:9200/customer

    • 请求体:无

    • 功能:根据索引名称删除索引和索引下的所有数据

    • 响应内容:

    • 补充说明:

      • ES中没有提供删除类型的操作,删除索引会自动删除所有类型,清空一个文档下的所有文档数据也会同时删除掉其所属的类型,实际上基于用法的感知上没啥区别,因为向某个类型下添加文档数据也需要知道对应类型的名字

     

BULK批量相关

  1. 【POST】http://192.168.56.10:9200/customer/external/_bulk

    • 请求体

      • 每两行是一个整体,请求体语法格式在补充说明部分给出,index表示这是批量新增操作,{"_id":"1"}是在URI中已知索引和类型的情况下指定当前数据的id,实际上完整的内容为{ "index": { "_index": "customer", "_type": "external", "_id": "1" }},第二行是完整的文档数据

      • 请求体的数据类型还是选择json,虽然PostMan会标红

      • 注意使用PostMan发送该请求,后一行后面要加一个回车才行,使用kibana不需要加回车

    • 功能:批量操作数据

    • 响应内容

      • "took": 134表示该批量操作耗时134毫秒

      • "errors": false表示过程中没有发生任何错误

      • items保存批量处理中每个处理的对应响应结果,index表示本次操作是一个保存操作,接着是三个元信息,版本号、操作结果、分片信息、版本号相关信息、"status": 201是该操作的状态码,表示刚新建完成

    • 补充说明

      • 批量操作的每一条记录都是独立的,上一条文档数据的操作失败不会影响下一条数据的操作

      • 请求体数据的语法格式

        • action是操作类型,metadata是一个文档数据的原数据信息如索引、类型和id

        • 第二行紧跟完整的文档数据

        • 随后循环添加要批量执行的其他操作

        【action的所有类型,偶数行是对应的文档数据,删除操作不需要文档数据】

      • 在uri中指定了索引和类型就是针对具体索引及类型下的操作,没有指定就是针对整个ES的操作,需要在请求体中指定索引和类型

      • ES官方提供的批量处理测试数据,原网址数据已经找不到了,这里从谷粒商城评论区找的移动到自己的gitee上的https://gitee.com/earl-Li/ES-bulk-testdata/blob/master/%E6%95%B0%E6%8D%AE,注意这个数据的元数据不含索引和类型,必须要自己在URI中进行指定,否则执行报错,这里的URI使用【POST】http://192.168.56.10:9200/bank/account/_bulk,使用官方的批量操作测试数据进行批量操作API的测试

  2. 批量删除示例

    【Kibana命令示例】

    【响应结果】

     

 

检索相关


ES的检索支持两种方式:

  • 第一种方式是在uri中直接添加请求参数

  • 第二种方式是在请求体中添加请求参数


  1. 【GET】http://192.168.56.10:9200/bank/_search?q=*&sort=account_number:asc

    • 请求体:无【使用uri中直接添加请求参数的方式,所以请求体无】

    • 功能:查询索引bank下的所有数据并将查询结果按照字段account_number进行升序排列

    • 响应内容

      • took- Elasticsearch执行搜索的时间( 毫秒)

      • time_out - 告诉我们搜索是否超时

      • _shards - 告诉我们多少个分片被搜索了, 以及统计了成功/失败的搜索分片

      • hits - 搜索结果

      • hits.total - 搜索结果整体信息,value是有多少条记录被搜索到

      • hits.hits - 实际的搜索结果数组( 默认为前 10 的文档),包含文档数据的元数据信息,当前文档的得分,_source原文档数据,sort是排序,从0开始;ES一次只会最多返回前10条数据,不会一次性返回所有数据,

      • sort - 结果的排序 key( 键) ( 没有则按 score 排序)

      • scoremax_score –相关性得分和最高得分(全文检索用)【因为本次查询就是查所有,不涉及模糊匹配等过程,所以没有评分数据】

    • 补充说明

      • 请求参数中q=*表示查询所有,sort=account_number:asc表示查询数据按照字段account_number的值升序排列

  2. 【GET】http://192.168.56.10:9200/bank/_search

    • 请求体:

      • query表示查询条件,match_all是进行精确匹配,匹配全部内容后面写写一个空的大括号

      • sort表示设置排序规则,按照account_number进行升序排列;排序规则是一个数组,可以多个排序规则组合使用,如第二个查询请求体,对应的排序规则是先按照account_number字段升序,在account_number相等的情况下再按照balance字段降序

      • 排序规则可以简写为字段: 排序方式的方式,如第三个查询请求体所示

      • 可以通过fromsize指定当前页第几位开始的文档数据和当前页的总记录条数

    • 功能:查询索引bank下的所有数据并将查询结果按照字段account_number进行升序排列

    • 响应内容:

    • 补充说明:

      • 这种将查询条件封装到请求体中的方式被ES称为Query DSL【领域对象语言】,ES官方文档中专门对Query DSL开了一个章节进行介绍,也是ES中最常用的查询方式,即请求体中封装查询参数的部分称为Query DSL

 

 

Query DSL的语法

ES提供一个可以被执行查询的JSON风格的DSL【domain-specific language 领域特定语言】,被称为Query DSL

  1. 一个查询语句DSL的典型结构

    • QUERY_NAME是指定查询操作,查询操作非常多,在kibana中可以看到相应的提示,这里只介绍常用的,完整的列表以后再补

    • ARGUMENT: VALUE是对查询操作的进一步配置

  2. 针对一个字段的DSL典型结构

  3. 常用的查询DSL以及相关QUERY_NAME举例

    • query定义查询的方法,match_all表示查询所有内容

    • sort表示查询结果列表的排序方式,可以多字段组合排序,表示在前序字段相等的条件下后续字段依次内部排序,前序字段不等以前序为准

    • fromsize组合完成分页功能,from表示第一个结果在排序列表中的位次,size表示当前页显示的记录条数

    • _source指定返回结果只包含指定的字段,属性值为字段名数组,不写表示返回完整文档

  4. query.match的用法

    一般的使用规定是,查询非text字段都使用term进行查询,文本字段的全文检索使用match来进行查询,多词条字符串的精确全文检索用match_phrase,字段值的完全精确匹配用match查询中的keyword

    • 基本类型【非字符串】的精确匹配

      • 表示精确匹配索引bank下account_number字段等于20的文档记录,这个20用字符串或者单纯的数字都是可以的

    • 字符串单个单词的全文检索

      • match检索字符串时会进行全文检索,查询出address字段包含对应字符串milld的所有记录,并给出每条记录的相关性评分

    • 字符串多个单词的全文检索

      • 这种以空格分隔的字符串,ES会将检索字符串进行分词、词条过滤处理后再分别到倒排索引表中进行匹配,最终查询出address字段中包含mill或者road或者mill road的所有记录,并给出相关性得分

    • 字符串的字段值精确匹配

      • 字段.keyword会让字段值完整精确匹配检索字符串,必须字段值完全等于检索字符串才会被查询到

  5. query.match_phrase的用法

    • 字符串多个单词不分词进行全文检索

      • 查出字段address中包含mill road的所有记录并给出相关性评分,注意是否区分大小写要看分词器的具体类型,默认的是不区分大小写的

  6. query.multi_match的用法

    • 查询多个字段同时包含指定字符串的查询

      • 查出字段state或者address有一个或者同时包含字符串mill的文档记录,注意这种方式的检索字符串也是会分词的,即"query": "mill road"会分词查询出对应字段包含mill或者road的文档记录

  7. bool的用法

    bool用于复合查询,用法是合并任何其他查询语句即QUERY_NAME,复合语句可以相互嵌套,可以组合出非常复杂的逻辑

    • must表示必须满足must列举的所有条件,示例如下

      • 查询同时满足address字段含有字符串millgender字段含有字符串M

      • range表示筛选出字段满足指定范围的文档记录

      • term的用法

        • term会精确匹配对应的检索词条,而且在对text类型的字段【即字段值为字符串类型】的时候,由于文档进行了分词,但是term中的检索词条不会进行分词,即便文档对应字段数据和term的检索字符串一模一样,也无法检索到属性值相同的那个文档数据,因此term常用来做非text字段的精确匹配,注意经过测试是精确匹配,而且只会精确匹配非text类型的字段

    • should的查询条件不会影响查询结果,只会影响查询结果的评分,满足should中查询条件会增加文档的评分,如果query中只有shouldshould中只有一种匹配规则,should的条件会作为默认匹配条件改变查询的结果

      • 查询索引bank下同时满足address字段含有词条millgender字段含有词条M的文档记录,优先展示address字段含有lane词条的文档记录

    • must_not表示查询到的文档必须满足不是指定的查询条件

      • 查询索引bank下同时满足address字段含有词条mill以及gender字段含有词条M,且email字段不含有词条baluba.com的文档记录,优先展示address字段含有lane词条的文档记录

    • filter的用法

      bool中的must和should中的查询条件满足是会增加文档相关性评分的,must_not中的条件不会影响相关性评分;filter中的条件也不会影响相关性评分

      • filter中的条件可能和must中的条件一致,比如range要求筛选出某个字段在一定范围内的文档记录,请求的写法分别为

        【must筛选范围】

        • must中没有match只有这个range也会有相关性得分

        【filter筛选范围】

        • filter会将记录中不满足预设条件的文档记录直接过滤清除掉

        • 满足filter中范围条件的文档记录的_score字段每条记录都为0,这是因为只进行了filter过滤,filter本身不计算得分,如果filter还组合了其他如should等条件,得到的记录还是会有相关性评分,比如以下这个含must的还是有评分数据的

 

Aggregation聚合分析相关

聚合功能是ES提供的数据分组和提取数据的功能,聚合类型比较多,有三类好几十种,这里主要讲terms和avg,其他的一些常用聚合函数后边用到再总结

  1. 聚合查询语法

    • aggregation_name是聚合操作的具体名称,aggregation_type是指定聚合操纵的类型,aggregation_body是指定聚合体【聚合体一般都指定field属性表示要聚合的字段以及size指定要展示的数据条数】,meta是指定聚合操作的元数据

    • 一个总的聚合查询语句中可以指定多个平行的聚合操作如aggregation_name_2,也可以使用第一次聚合aggregation_name的结果再次发起聚合操作sub_aggregation【注意啊,这个操作只是把上次聚合的结果作为新聚合操作的分组,实际上你可以在年龄分组操作后再对同一个分组的薪资进行聚合操作】,sub_aggregation称为子聚合

    • aggregations可以缩写为aggs

  2. 一个查询操作多个平行聚合操作

    • 搜索bank索引中的address字 段包含mill的所有人的年龄分布以及平均年龄,但是不显示对应的记录详情

      • aggs表示对query中的查询结果执行聚合操作,group_by_state是当前聚合的名字,term是一种聚合的类型AGG_TYPE,其他的聚合类型包括avgterms

      • size为0表示不显示query对应的即hits中的搜索数据

    • 响应结果

      • 其中hits中显示query查询的结果,因为size为0所以不显示查询结果,注意这个size也可以在aggs中的聚合类型内部使用,作用是只显示聚合结果中的前size条数据

      • aggregations中显示聚合操作的结果,每个聚合操作的json结果都以聚合操作的名字作为json对象的名字,doc_count_error_upper_bound是聚合中发生的错误信息,sum_other_doc_count是本次聚合操作统计到的其他文档的数量,buckets意思是桶,桶中的每个json对象都是一个统计结果,key表示一个结果的统计值,doc_count表示当前统计值下的文档记录数量,比如年龄为38的文档有2个,avg_age是第二个聚合操作的结果,value显示当前所有文档的平均年龄

  3. 一个聚合操作含有子聚合操作的查询

    • 按照年龄进行分组聚合,并对每个年龄分组求这些年龄段的人的平均薪资

    • 响应结果

  4. 对文本字段的聚合操作需要使用字段.keyword,同时子聚合中使用多个并行聚合操作

    • 对所有年龄分组, 并且这些年龄段中字段gender为M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资

    • 响应结果

       

Mapping映射

字段类型



核心类型
  1. 字符串

    • text

    • keyword

  2. 数字类型

    • long

    • integer

    • short

    • byte

    • double

    • float

    • half_float

    • scaled_float

  3. 日期类型

    • date

    • date_nanos

      • 兼容纳秒的日期类型

  4. 布尔类型

    • boolean

  5. 二进制类型

    • binary

复合类型
  1. 数组类型

    • Array

  2. 对象类型

    • Object

      • Object类型用于单JSON对象

  3. 嵌套类型

    • nested

      • nested用于JSON对象数组

地理类型
  1. 地理坐标Geo

    • 地理坐标Geo-points

      • Geo-points用于描述经纬度坐标

    • 地理图形Geo-Shape

      • Geo-Shape用于描述多边形等复杂形状

 

特定类型
  1. IP类型

    • ip用于描述ipv4和ipv6

  2. 补全类型Completion

    • completion提供自动完成提示

  3. 令牌计数类型Token count

    • token_count用于统计字符串的词条数量

  4. 附件类型attachment

    • 参考mapper-attachements插件,支持将附件如Microsoft Office格式、Open Document格式、ePub、HTML等等索引为attachment数据类型

 

多字段muti-fields
  1. 概念

    • 为了满足业务场景使用不同的方法同时索引同一个字段

      • 如String类型字段可以同时映射为一个text字段用于全文检索,或者一个keyword字段用于排序和聚合,此外text字段还可以被各种类型的分析器standard analyzer、english analyzer、french analyzer来进行分词并建立索引

 

 

映射Mapping

  1. 概念

    • Mapping用于定义一个文档所包含的属性field是如何存储和被索引的,使用mapping可以定义:

      • 哪些字符串属性应该被看做全文本属性full text field

      • 哪些属性为数字类型、日期类型或者地理位置类型

      • 文档中所有属性是否都能被索引

      • mapping还可以定义日期的格式

      • 自定义映射规则来执行动态添加属性

    • 索引一个文档,文档数据的类型会被ES自动进行类型猜测,这些映射可以在索引数据后修改,也可以在索引数据前进行指定

      • 数字都会被猜测为long

      • 字符串都会被猜测为文档text,且每个文本默认都会有对应的keyword子类型

API
  1. 【GET】http://192.168.567.10:9200/bank/_mapping

    • 请求体:无

    • 功能:查看索引下的映射信息

    • 响应内容

      • properties会显示所有字段的类型

      • text类型会自动进行全文检索,对对应的文档信息进行分词分析,同时一个字段还可以有子类型fields,表示address字段还可以是keyword这种类型,表示该字段值可以被完全精确匹配

  2. 【PUT】http://192.168.56.10:9200/my-index

    • 请求体:

      • 可以直接指定字段数据类型为keyword,注意这种指定方式指定的text没有子数据类型keyword,指定为keyword的类型和text类型也没有关系

    • 功能:创建索引my-index的同时为索引指定映射规则

    • 响应内容

      【索引映射】

    • 补充说明

      • 不能再次使用该请求并在请求体中修改映射规则如请求体如下,实际上不更改映射规则也会报错,报错信息是目标索引已经存在

        • 响应内容

      • 每个映射的数据类型在定义的时候都默认添加了"index": true,即

        • 意思是当前字段属性会被索引并能被检索,如果将index设置为false,则该字段不会被索引,也无法通过该字段索引文档,该属性只是作为文档的冗余存储

  3. 【PUT】http://192.168.56.10:9200/my_index/_mapping

    • 请求体

    • 作用:为索引新增映射

    • 响应内容

      【此时对应索引下的映射信息】

    • 补充说明

      • 注意这种方式不能用于修改当前已经存在的映射关系,如不能把email的数据类型改为text

      • 官方规定了已经存在的映射关系是不能修改的,变更一个已经存在的映射可能会导致已经存在的数据失效【比如检索规则】,如果是在需要变更某个字段的映射关系,官方建议创建一个新的索引并设置新的映射规则,并且索引老索引下的所有旧数据到新索引下,也即把旧数据迁移到被设置正确映射关系的新索引下

  4. 【POST】http://192.168.56.10:9200/_reindex

    • 请求体:

      【旧索引不含类型的情况】

      • 该WEB API的作用是在两个不同的索引间迁移所有的数据

      • dest表示设置目标索引的位置、source表示旧索引的位置,index属性都填写对应的索引名

      • 注意啊,经过测试,原来的索引有mapping映射而且新索引也有不同的mapping映射也一样可以通过该方式进行数据迁移,不需要指定类型

      【旧索引包含类型的情况】

      • 即需要指定旧索引的索引和类型,新索引只需要指定索引不需要指定类型

      • :如果一个索引下有多个类型怎么办,可以把type写成数组吗

      • :如何查询一个索引下的全部文档类型

       

分词器Tokenizer



分词相关API

  1. 【POST】http://192.168.56.10:9200/_analyze

    • 请求体:

    • 功能:使用标准分析器对指定文本进行分析并响应分析结果,analyzer是指定分析器,常用的分析器有standardik

    • 响应内容:

    • 补充说明:

      • 标准分析器对英文文档会以空格作为标准对文档进行分词,对待中文的处理方式是直接分词到字,这种方式很不好,而且ES中内置的大多数分析器都是针对英文的,一般对中文的分析都使用ik分词器

 

ik分词器



  1. 测试ik_smart分词器

    • 【POST】http://192.168.56.10:9200/_analyze

      • 请求体:

      • 功能:使用ik分词器的ik_smart分词器分析文档我是中国人!,该分词器会尽可能按文档意思按最粗粒度进行分词,但是不会分词到字

      • 响应结果:

      • 补充说明:

        • 标点符号是不进行索引的,默认无法识别自定义词

  2. 测试ik_max_word分词器

    • 【POST】http://192.168.56.10:9200/_analyze

      • 请求体:

      • 功能:使用ik_max_word分词器分析我是中国人!,该分词器会尽可能找到每一个短语,即便每个字被多次使用,但是不会分词到单个字

      • 响应内容:

      • 补充说明:

        • 默认无法识别自定义词,如尚硅谷

 

自定义ik扩展词库



  1. 在nginx上搭建远程词库

    • 1️⃣:参考整合Elasticsearch--环境安装安装nginx容器实例

    • 2️⃣:在容器数据卷nginx的静态资源目录/malldata/nginx/html下创建es目录专门存放ik分词器使用到的远程词库,在es目录下创建文件ik_remote_lexicon.txt,在文件中输入以下词条

      • 本质上是将词典从本地弄成网络资源供ik分词器自己去获取,区别只是ik分词器从ES本地获取或者从网络获取

    • 3️⃣:使用命令vi /malldata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml修改ik分词器的配置文件内容如下

      • 打开配置远程字典的配置注释,填入词典的URL地址http://192.168.56.10/es/ik_remote_lexicon.txt

    • 4️⃣:使用命令docker restart elasticsearch重启容器实例

    • 5️⃣:使用ES的ik分词API尝试索引包含对应词条的文档,观察相应词条是否索引成功

      • 【POST】/analyze

      • 响应结果

         

Elasticsearch-Rest-Client



  1. 搭建操作ES的模块

    • 1️⃣:创建模块mall-search,勾选整合Web中的Spring Web

      • 说明:NoSQL中有个Spring Data Elasticsearch因为最新只整合到6.3版本的ES【当时ES的最新版本是7.4】,所以就不考虑SpringData Elasticsearch,如果ES使用的版本不是那么新,选择SpringData Elasticsearch其实也是很好的选择,相比于官方的Elasticsearch-Rest-Client做了更简化的封装

    • 2️⃣:导入Java High Level REST Client的maven依赖,将版本号改为对应ES服务器的版本号,将ES服务器的版本号在properties标签中进行重新指定

      • 注意通过右侧的maven依赖树能够看到elasticsearch-rest-high-level-client虽然版本是7.4.2,但是子依赖中的部分版本还是6.8.5,这是因为SpringBoot对ES的版本进行了默认仲裁,SpringBoot2.2.2.RELEASE当引入SpringData Elasticsearch会自动仲裁Elasticsearch的版本为6.8.5【点开父依赖中的spring-boot-starter-parent的父依赖的spring-boot-dependencies能够看见相关的版本信息】

      • 更改SpringBoot对Elasticsearch的版本自动仲裁,刷新maven直到依赖树中的相关依赖版本全部变成7.4.2

    • 3️⃣:对rest-high-level-client进行配置

      • 🔎:如果使用SpringData Elasticsearch对ES操作,配置就非常简单,这个在ES的整合SpringData Elasticsearch中已经实现了,这里要配置我们自己选择的rest-high-level-client会稍微复杂一些

      • 编写配置类MallElasticsearchConfig并注入IoC容器,这个配置类参考ES的官方文档Java High Level REST Client中的Getting started中的Initialization

        • 需要创建一个RestHighLevelClient实例client,通过该实例来创建ES的操作对象

        【单节点集群的创建客户端实例】

        【多节点集群下的创建客户端实例】

        • 多节点集群就在RestClient.builder(HttpHost...)方法中的可变长度参数列表中输入各个节点的IP信息

    • 4️⃣:导入模块mall-common引入注册中心【这里面引入的其他依赖挺多的,包含mp、Lombok、HttpCore、数据校验、Servlet API等】,配置配置中心、注册中心,服务名称在主启动类上使用注解@EnableDiscoveryClient开启服务的注册发现功能,在主启动类使用@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)排除数据源

      【配置中心bootstrap.properties配置】

      • 注意bootstrap.properties文件必须在引入nacos的配置中心依赖后才会展示出小叶子图标

    • 5️⃣:编写测试类检查ES操作对象是否创建成功

      • 只要能打印出client对象,说明成功连接并创建ES操作对象,后续只需要参考官方文档使用对应的API即可,对应的文档也在Java High Level REST Client中的所有APIs部分

Java High Level REST Client API

RequestOptions



  1. 将RequestOptions添加至mall-search模块的统一配置类中

    • 配置实例

       

Document APIs

  1. 索引文档到ES

    • 使用IndexRequest对象来索引一个文档,创建IndexRequest对象时指定索引名称,也可以创建IndexRequest以后指定,IndexRequest对象中大部分属性都有对应的同名方法来传参对应的属性值,如index和id等;ES服务器中没有对应索引会自动创建同名索引;而且该api是保存更新操作二合一,多次操作会更新版本号

    • 可以使用各种工具来将文档数据对象转换为json格式的字符串

    • indexRequest的source方法是传参文档数据,支持如下多种参数类型,最常用的是直接传参一个json字符串

      • 文档数据参数类型1:JSON字符串

        • 注意传递JSON格式的数据一定要指定内容类型为XContentType.JSON,否则会报错提示传参的Object对象只有一个

      • 文档数据参数类型2:Map集合

      • 文档数据参数类型3:K-V键值对构造的XContentBuilder,直接在大括号内用XContentBuilder的相关方法传递键值对数据

      • 文档数据参数类型4:可变长度参数列表直接传入键值对

    • 传参JSON字符串代码实例

    • IndexRequest中还可以设置文档保存超时时间、刷新策略、版本号等等

    • 文档数据的保存可以分为同步和异步两种方式,同步是等待保存操作执行结束再继续执行后续代码,异步是不等待数据继续执行后续代码用监听器监听响应后执行回调,暂时先不考虑异步的问题,上述代码使用的是同步的索引文档操作

       

Search APIs



  1. 检索和聚合文档记录

    • 使用searchRequest.source(searchSourceBuilder);来封装检索条件

    • esRESTClient.search(searchRequest,MallElasticSearchConfig.COMMON_OPTIONS)来执行检索操作

    • searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));来封装词条检索条件

    • searchSourceBuilder.from();searchSourceBuilder.size();来封装分页操作

    • searchSourceBuilder.aggregation(AggregationBuilders.terms("ageByGroup").field("age").size(10));来封装值分布聚合操作条件

    • searchSourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));来封装均值聚合操作条件

    • 带条件检索和Terms值分布和AVG均值聚合操作的代码实例

      • 以下代码前半部分是发起带聚合的检索操作

      • 后半部分是获取响应的记录以及聚合操作的结果

 

must函数用法

  1. 示例代码1️⃣

    • 请求参数http://localhost:12000/list.html?keyword=华为&catelog3Id=225&attrs=11_海思芯片:Apple芯片&attrs=12_HUAWEI Kirin 980:M1

    • 这种方式也可以使用在要求满足每一种属性可能的属性值条件的场景,这个方式也是可用的;经过验证这种方式不能用啊,这个语句很神奇啊,must下只有任意一个bool语句都能正常检索,但是must下同时有这两个bool会直接什么都查不出来啊,而且看起来逻辑也是对的,这是框架底层的原因,不要深究,直接用第四种

    • 对应DSL

      • 可以看到,同一个QueryBuilders.boolQuery()不论调用多少个must(),都是放在一个"must"语句中

  2. 示例代码2️⃣

    • 请求参数http://localhost:12000/list.html?keyword=华为&catelog3Id=225&attrs=11_海思芯片:Apple芯片&attrs=12_HUAWEI Kirin 980:M1

    • 对应DSL

      • 可以看到,我们希望每个属性的属性id和可能的属性值关联起来,但是由于对同一个nestedBoolQueryBuilder调用must方法,导致所有的属性id和属性值都关联到一个must中去了

  3. 示例代码3️⃣

    • bool语句隔开must,用should语句对各个must求或运算

    • 请求参数http://localhost:12000/list.html?keyword=华为&catelog3Id=225&attrs=11_海思芯片:Apple芯片&attrs=12_HUAWEI Kirin 980:M1

    • 对应DSL

      • 这个是能正常使用的,但是如果能有只构建"query"语句不带bool语句的API就更好了

      • 不对啊,这个也不能用啊,因为要求是选出满足对应属性的商品,如果使用或就会导致商品只要满足一个属性就能被展示出来,但是实际上要求商品满足用户选择的所有属性,因此这里的should应该换成must,即第一种情况

  4. 示例代码4️⃣

    • 请求参数http://localhost:12000/list.html?keyword=华为&catelog3Id=225&attrs=11_海思芯片:Apple芯片&attrs=12_HUAWEI Kirin 980:M1

    • 对应DSL

      • 这种相当于用filter语句来过滤掉每一个属性匹配条件,这种也是可以用来处理同时满足所有商品属性

       

 

检索代码示例

  1. 通过Java代码自动构建DSL

    • 映射关系

    • 文档数据

    • 检索逻辑

      • 检索商品名称包含指定关键字、商品分类id为指定值、品牌id为指定值列表、商品属性为指定值且价格范围在指定范围的所有商品记录并按照价格降序排列,对查询到的所有商品记录聚合分析出包含的所有品牌id、品牌名、品牌商标;商品分类id、商品分类名称;所有属性id、属性名称和对应属性id下的全部可能存在的属性值,高亮检索关键字、记录分页

  2. 检索方法的基本结构

    • 控制器方法

    • 检索方法大结构

      • 整体结构为构建一个SearchRequest封装检索DSL语句,用RestHighLevelClient使用SearchRequest发送请求执行检索并返回检索结果,将检索结果封装成我们自定义的响应对象

      • 构建检索请求对象即构建检索DSL比较复杂,单独抽取成buildSearchRequest()方法,这方法有100行

      • 根据检索结果构建响应对象的过程也比较复杂,单独抽取成buildSearchResponse()方法

    • DSL检索根据逻辑手动构建的语句

      • 注意这个"skuTitle": "华为"可以写成"skuTitle": "华为,apple"同时检索文档skuTitle属性为华为或者apple的文档记录,只要包含华为或者包含`Apple的文档记录都可以被检索到

    • 请求路径http://localhost:12000/list.html?keyword=华为&catelog3Id=225&attrs=11_海思芯片:Apple芯片&attrs=3_LIO-AL00;是;否:LIO-AL00&skuPrice=12_6000

    • 构建检索DSL的buildSearchRequest()方法

      • 检索语句的构建需要通过对象SearchSourceBuilder进行,通过searchRequest.source(searchSourceBuilder)来封装检索条件

      • SearchRequest对象有一个双参构造方法public SearchRequest(String[] indices, SearchSourceBuilder source),第一个参数是指定从哪些索引中检索数据,第二个参数是构建DSL语句的SearchSourceBuilder对象

      [基本框架]

      • 封装查询条件需要使用QueryBuilders对象、封装聚合分析语句需要使用AggregationBuilders,并且通过SearchSourceBuilder对象的queryaggregation方法来分别封装这些builders

      [构建query语句]

      • 我这里的nested语句还是按照上面的DSL语句处理的,雷神的处理方式是在filter语句中创建多个nested语句,导致处理方式变化的原因是nestedBoolQueryBuilder.must()方法无法传参多个QueryBuilders,但是又需要同时包含term语句和terms语句,让termterms处于不同的must中就剥离了属性id和属性值的与运算;我这里的处理方式是在must中再构建一个query语句,在query语句中,实际上我的处理方法是不行的,细节看must语句的分析

      • 通过searchSourceBuilder.query(boolQueryBuilder)在构建好boolQueryBuilder后再构建最外层的query语句

      • 通过QueryBuilders.matchQuery("skuTitle",param.getKeyword())来构建在属性skuTitle中模糊匹配的关键字keyword的match语句

      • 通过QueryBuilders.termQuery("catelogId",param.getCatelog3Id())构建精确匹配非text属性catelogId的term语句

      • 通过QueryBuilders.termsQuery("brandId",param.getBrandIds())来构建一个属性brandId的多值匹配的terms语句,这个暂时也认为是非text精确匹配

      • 通过QueryBuilders.nestedQuery("attrs",nestedBoolQueryBuilder,ScoreMode.None)来构建nested类型属性attrs的nested匹配语句,nestedBoolQueryBuilder实际就是QueryBuilders.boolQuery(),ScoreMode.None是当前nested类型匹配不计入文档评分,返回

      • 通过QueryBuilders.boolQuery()构建bool语句,返回BoolQueryBuilder,通过该对象的上述方法构建或关系的查询匹配语句

      • 通过QueryBuilders.rangeQuery("skuPrice")构建针对属性skuPrice的range匹配范围语句,通过返回对象的gte方法指定范围区间低值,通过返回对象的lte方法指定范围区间高值,不指定高低值默认为null,分别对应0和无穷大

      • 通过boolQueryBuilder.filter(QueryBuilder queryBuilder)来在同一个filter语句中构建多个子查询语句,这里的queryBuilder一般通过上述方法获取

      • 通过boolQueryBuilder.must(QueryBuilder queryBuilder)来在bool语句下构建must语句,must语句中一般使用term或terms语句,同一个boolQueryBuilder对象多次调用must方法是在同一个must语句中添加子查询语句,这些子查询语句都必须同时满足的记录才会被检索出来

      [构建排序、分页、高亮]

      • 排序用searchSourceBuilder.sort(String path,SortOrder order)来设置path属性的排序规则

      • 分页用searchSourceBuilder.from(int fromIndex)设置首条记录的位置,用searchSourceBuilder.size(int size)设置每页记录数,即使没有设置分页参数也要设置默认分页数据让当前页码pageNum为第一页,定义一个每页商品记录数size常量;当用户指定页码后当前页pageNum为用户指定页码,DSL语句中的from=(pageNum-1)*sizesize就使用设置的常量即可,这里size设置为2是商品数据量较小,方便查看分页效果

      • 高亮只有有keyword对商品名称进行模糊匹配的时候才能用,通过searchSourceBuilder.highlighter(HighlightBuilder highlightBuilder)传参HighlightBuilder对象,通过HighlightBuilder.field(String field)指定对哪个属性的匹配关键字进行高亮,通过HighlightBuilder.preTags(String preTag)指定高亮标签前缀,通过HighlightBuilder.postTags(String postTag)后缀

      [聚合分析]

      • 聚合分析语句的子语句比如term语句、terms语句都由AggregationBuilders构建,

        • aggregationBuilders.terms(String aggName)是构建自定义聚合aggName下的terms语句,对应的field属性和size属性通过对应的同名方法指定,注意聚合没有term语句[因为term是单属性值匹配,不适合这种对属性值分组分析场景]

        • termsAggregationBuilder.subAggregation(AggregationBuilder aggregationBuilder)是构建子聚合,子聚合的具体内容一般也是通过aggregationBuilders.terms(String aggName)通过terms来做聚合

        • searchSourceBuilder.aggregation(AggregationBuilder aggregationBuilder)可以调用多个将多个聚合操作放在DSL语句中的同一个aggregations语句下

      • 对应代码构建的DSL语句

      • Kibana使用自动构建语句响应的检索结果

 

检索结果封装示例

  1. 自定义方法buildSearchResponse(SearchResponse response)封装检索结果为我们自定义响应对象

    • 业务逻辑

      • 封装所有查询到的当前页的商品数据、

      • 封装当前商品涉及到的所有属性聚合信息、品牌信息、商品分类信息

      • 封装分页信息-当前页码和总记录数

    • 检索响应结果对象的结构

      • Java High Level REST Client API的检索方法SearchResponse response = esRESTClient.search(searchRequest, MallElasticSearchConfig.COMMON_OPTIONS);检索响应结果对象的类型是SearchResponse对象,该对象中的internalResponse属性中封装了全部检索数据

      • searchResponse.internalResponse.hits中封装了检索命中的记录,hits内部的属性和检索结果的结构是完全相同的

        • 总记录数hits.getTotalHits().value获取命中记录的总记录数

        • 总页码需要自己根据总记录数和每页记录数自己运算得到,可以通过算法总页码=(int)(总记录数-1)/每页记录数+1进行计算

        • 当前页码需要使用请求参数中的页码数据

        • 所有记录通过hits.getHits()获取的是每一条记录的元数据+文档信息,其中的_source属性才是真正的文档数据,hits.getHits()获取的列表其中的每个元素hit可以通过hit.getSourceAsString()可以获取到json格式的文档数据字符串,通过fastjson转换为商品对象并加入list集合

      • searchResponse.internalResponse.aggregations中封装了聚合分析的结果,aggregations内部的属性和检索结果的结构是完全相同的

        • searchResponse.internalResponse.aggregations.aggregations这个ArrayList中封装了多个聚合结果,每个元素都是一个单独的聚合结果,聚合结构元素的类型不同,一般有ParsedLongTermsParsedNestedParseStringTerms

        • 通过searchResponse.getAggregations().get(String customAggregationName)可以获取对应聚合名字的聚合分析结果,注意这里的返回结果类型使用了多态,用了父类型Aggregation类型,但是我们需要子类实现来调用对应的子类方法,因为不同子实现类的聚合结果层级结构不同,父类型方法不够用

          • 注意Aggregation子实现类贼多,而且层级复杂,一般要通过打断点观察一下返回聚合结果的具体类型才能确定子实现类的类型

        • 通过aggregation.getBuckets()获取bucket中的聚合数据ArrayList数组,对数组中的每一个bucket元素通过bucket.getKeyAsString()获取到字符串类型的聚合分组属性值,通过bucket.getKeyAsNumber()获取到long类型的聚合分组属性值,通过bucket.getAggregations().get(String customAggregationName)获取到对应名字的子聚合,子聚合也通过上述方法获取到字符串类型的聚合分组属性值,如果子聚合确定只有一个属性值就不用对子聚合的bucket进行遍历了,可以直接通过aggregation.getBuckets().get(0)来获取bucket的key属性值

          • 注意Aggregation无法调用aggregation.getBuckets(),必须强转为子类才能调用该方法获取到聚合结果,这难道不是设计缺陷吗

    • 检索结果封装代码

      • 这种方式封装的商品标题匹配的检索关键字并没有被高亮,还需要对标题进行单独封装来保证商品名称检索关键字的高亮,要高亮还需要满足检索参数中有用户输入的检索关键字才会封装高亮数据,高亮数据在与source平级的hightlight属性中封装,直接替换掉source中对应的字段即可

    • 带高亮关键字的检索结果封装代码

 

 

ES在项目中的应用



  1. 腾讯云ES服务的环境架构

    • 架构说明

      • 服务器、移动设备、或者物联网传感器产生的日志通过Kafka或者LogStash搜集到ES服务器中,通过可视化工具来对日志进行检索和监控

    • 架构图

 

 

商品上架

  1. ES做sku全文检索分析

    • 需要将sku商品信息存储在ES服务器上,将sku信息存入ES的过程称为商品上架,只有上架的商品才能在商城界面展示出来,没有上架的商品只能在后台管理系统看见

    • 使用ES做全文检索而不采用mysql的原因是mysql的全文检索功能没有ES的强大,mysql做复杂全文检索的性能远不及ES,ES将数据存储在内存中,性能远高于mysql;其次,ES天然就支持分布式集群,如果当前集群内存不够,直接像集群中添加ES节点即可

    • 由于内存比较贵,虽然商品上架点击的是spu管理中的上架按钮,但是为了节省内存,只向ES中保存商城搜索页面会使用到的数据【商品展示数据和用于检索的数据】,类似于商品图片,细节的sku属性,spu属性等商品详情页面都等拿到商品的sku_id后,商品的完整介绍、全部图片、完整的信息再去数据库直接查

  2. 业务逻辑

    • 1️⃣:后台管理界面商品维护菜单下的spu管理中spu列表每条记录有对应的上架按钮,点击上架按钮一方面将商品的spu_info表中的状态字段publish_status改为上架状态【0表示未上架状态,1表示上架状态】,另一方面对应的商品数据保存至ES服务器中

    • 2️⃣:分析需要存储到ES的具体数据

      • 检索词条商品标题和副标题

      • 用户可能按照商品sku的价格区间进行检索

      • 可能按照商品的销量进行排序,以上都属于商品的sku属性

      • 可能直接点击商品分类检索跳转商品页面,这是按照商品分类进行检索,需要保存商品的分类信息

      • 可能直接点击商品的所属品牌,按照品牌进行检索,需要保存商品的品牌信息

      • 可能按照商品额规格属性进行检索,如屏幕尺寸、CPU类型等等,因此还需要保存商品的spu属性

    • 3️⃣:文档数据存储方案

      • 设计1:如果每个商品都设计为如下的文档类型,这种设计的好处是方便检索,但是可能会存在大量的数据冗余,因为同一个spuId下存在大量的sku商品,但是这些商品的spu属性都是相同的,如果有100万个商品,平均每个商品有20个sku,假设每个sku的重复数据为100Byte,需要额外1000000*2KB=2000MB=2GB,一般来说,这个冗余量还是比较好处理,即使是20G的冗余也比较容易处理,加一根内存条就能解决

      • 设计2:如果商品设计为下列文档类型,这种方式检索也比较方便,数据也不会出现冗余存储;但是这种方式有一个致命问题,检索条件的spu属性是统计当前检索商品文档对应的所有spu的可能属性动态生成的,意味着比如检索一个品牌如小米,需要检索出商品名字含小米的全部商品,并且查询出所有的spuId,假设1000个商品对应4000个spu,需要单次网络传输传递4000个Long类型的id,每个Long类型数据占8个字节,单次查询spu属性的请求体数据大小为4000*8Byte=32KB,一个请求就会发送超过32KB的数据,如果是10000的并发,每秒内网传输的数据就是320MB的大小,像超大型电商平台百万的并发,内网传输的数据将会变成32GB,这将会造成极大的网络阻塞,而且还没有考虑其他请求的情况下【单是拆分出spu单独检索就会产生的额外开销】,因此考虑第一种冗余设计,以空间换时间

    • 4️⃣:商品文档数据映射设计

      • 索引product映射设计

        • skuId:用于用户点击后查询商品详情

        • spuId:用户点击商品后查询商品的spu信息,spu还会涉及到一个数据折叠功能,因此设计为keyword类型,后面讲

        • skuTitle:商品的标题,商品的副标题不进行保存,因为一般用户检索也是检索标题,注意只有商品标题才需要全文检索,因此只将标题设置为text类型并使用ik_smart分词器

        • skuPrice:做商品价格区间统计聚合使用,为了精度问题设计为keyword

        • skuImg:设置为"index": false,让该字段不可被检索,但是可以被作为文档数据查出来,"doc_values": false意思是该字段不需要做聚合操作,冗余存储的字段都可以设置这两个属性来达到节省内存空间的目的【不会被索引和做一些聚合相关的其他操作】

        • saleCount:商品销量用于销量排序

        • hasStock:是否有库存,用于用户点击仅显示有货按钮检索有库存的商品,存布尔类型,这样的好处是不需要频繁地更新文档数据【文档数据只要修改,ES就会重新索引文档,频繁更新就会极大增加维护索引的开销】,只有在商品没库存的时候才更改ES中的文档数据

        • hotScore:商品热度评分,用来表征商品的访问量

        • brandId:品牌id,用于商品按照品牌名进行检索

        • catalogId:商品分类id,用于商品按照商品分类进行检索

        • brandName、catalogName、brandImg:这三个字段用于展示对应的品牌图片、名字和商品分类名称,只需要被展示,不需要检索和聚合

        • attrs:当前商品的属性规格,nested表示attrs是一个数组【这个nested非常重要,不使用nested会出问题,因为spu属性的个数是未知的】,数组的每个元素包含attrId、attrName、attrValue

  3. 业务实现

    • 前端点击商品系统spu管理的上架按钮,发送请求http://localhost:88/api/product/spuinfo/${spuId}/up到后端接口

    • 创建ES中product索引文档数据对应的java实体类,注意,因为product模块和search模块都会使用该java实体类,选择将该实体类创建在common模块下,但是实际的微服务开发中,写search模块的哥们根本拿不到common模块的权限,实际上都是product模块中写一个product对应实体类,search模块再写一个相同的实体类

      • 实体类【根据文档映射创建,JavaBean的属性类型与对应数据库实体类的属性类型保持一致,数据库没有的自定义,属性类型用静态内部类创建】

    • 后端使用spuId获取spu下的所有skuInfo实体类,对sku信息进行属性对拷,其中属性名不同的有skuPrice【数据库表中为price】,skuImg【skuDefaultImg】,这两个单独拷贝即可;非skuInfo的数据包含hasStock、hotScore、brandName、brandImg、catelogName、Attrs,是否有库存需要给库存模块mall-ware发送请求查询是否有库存,热度评分默认设置为0【但是实际上新品上市热度评分应该要给比较高的权重,实际上热度评分应该是后台一个比较复杂的操作】,单独查询品牌和商品分类的名字并进行拷贝【注意这里还是循环查库,据说后面会优化】,查出当前sku所有的可以被检索的spu规格属性【因为一个sku的所有商品的spu属性都是一样的,所以可以在循环外实现查出来后在循环中进行属性对拷】,先通过spuId从表pms_arrt_value查出对应的所有spu属性并拿到所有的属性id,拿着所有的属性id从表pms_attr中筛选出字段为search_type为1表示可检索的属性【这个用xml中手写sql实现】,对应SQL语句为

      • resultType为返回值的对应类型

    • 将查出的满足搜索条件的属性id转换成一个Set集合new HashSet<>(List集合)可以直接将List集合转换成set集合,对pms_arrt_value查出的数据进行过滤,检查对应的元素的属性id是否在set集合中,在则过滤出来作为商品的规格属性封装到ES文档数据中,属性表的字段名和ES文档类中的Attr的属性名都是相同的,可以直接属性对拷

    • 远程查询库存系统是否还含有库存的业务实现,,远程调用也是传递skuId来调库存模块获取每种商品是否含义库存,如果循环调用,性能非常慢,需要一次调用,获取所有skuId的库存状态;在ware模块中写一个通过skuId集合查询表wms_ware_sku表对应sku是否含有库存的接口,用实体类传递响应内容,用map多客户端协调不容易知道响应内容,用Vo不需要浏览代码就知道响应内容;用wms_ware_sku表中商品总库存数量减去锁定库存的数量是否大于0来判断商品是否有货【锁定的库存是用户已经下单但是还没有支付完】,SUM()计算两个字段的加减运算,返回数据可以使用泛型来标记响应数据的类型,这样数据可以直接自动转换到存入类型,而不需要自己拿到数据再进行强转,当然,当前模块还是需要一份对应的Vo或者To类,renren项目的响应R是没有涉及泛型的,远程调用查询网络可能存在波动,用try,catch来应对万一发生的网络波动,失败就打印日志,不管失败还是成功都要继续执行上架的功能【没有库存了还是上架并设置仍然有库存】

    • 将数据使用mall-search模块发送给ES服务器进行保存,代码接口/search/save/up/product,批量操作进行保存,全部保存成功即成功,没有全部成功返回商品上架异常错误码,但是已经上传的商品仍然上架

    • 此时还存在问题,第一商品上架重复调用即上架服务接口幂等性的问题【search模块中商品上架失败要考虑是否需要重试,多次调用是否以及如何保证幂等性问题】

  4. 主要代码

  5. 远程调用商品上架服务,调用失败会有重试机制,服务调用用的是Feign的重试机制,SynchronousMethodHandler中调用RetryercontinueOrPropagate重试代码如下

    • 当第一次尝试this.executeAndDecode(template)发生了异常,异常被捕获并调用retryer.continueOrPropagate(e)来进行重试

    • retryer.continueOrPropagate(e)如果重试的最大次数超过maxAttempts【默认值是5】,则抛异常e被invoke方法捕获

    • 如果没超次数,retryer.continueOrPropagate(e)执行后,由于while(true)死循环,该方法还会继续执行,直到抛异常或者执行成功才会从该循环中跳出

    SynchronousMethodHandler的invoke代码】

    【Retryer部分代码】

    SynchronousMethodHandler的invoke代码】

     

 

nested数据类型



数组扁平化处理
  1. 请求体提交数据

  2. 将在ES中被处理转换后的文档

    • 即将数组每个对象的属性名和数组名结合起来,将对象的每一个属性作为一个数组

  3. 这种方式存在的问题

    • 因为数组被扁平化处理了,在用户层面理解的检索条件如

    • 检索结果

    • 数组的默认映射是将user.first作为一个带keywordtext,并不是nested

    • 用户需要找到同时满足"user.first": "Alice""user.last": "Smith"的数据,从存储的数据上来看并没有满足这样要求的数据,但是实际运行还是会得到全部的文档数据,因为数组被扁平化处理了,ES只会去检索数组user.first中是否含有Alice,同时去检查user.last数组中去检查是否含有Smith,发现该文档两个数组都满足检索条件,就会返回该文档数据,实际上这两个条件并不同时满足在同一个对象的要求,即数组被处理的无法处理同一个对象的属性之间的联系,这样会发生检索错误的问题,需要使用nested数据类型【注意这种问题一般发生在数组元素是对象的情况下,数组元素是单个值不会发生这样的情况】

  4. 使用nested类型来定义索引的映射关系

    • 此时再向该索引下索引对应的文档数据并使用相同的条件检索文档,不会再发生检索出错误数据的问题

  5. 检索结果

  6. 此时的映射关系为

    • 只是给user即数组元素对象增加了一个nested类型,增加以后的效果是不会发生扁平化处理产生的不能区分同一个对象属性联系的问题,但是没有讲明具体的区分原理,通过WEB APIGET my_index/_mapping可以查看哪些属性被设置为哪种类型

    • 因此数组元素为多属性对象时一定要在索引映射中声明该对象是一个nested数据类型

nested数据查询
  1. nested数据类型的查询

    • 如果一个属性被声明为nested类型,通过该属性查询文档必须使用nested对应的查询语法进行查询,否则查询不到文档数据

    1️⃣[nested类型数据使用原始查询方式查不到数据示例]

    [响应内容]

    • 通过普通方式查询nested类型的数据没有查到一条记录

    2️⃣[nested类型数据正确查询方式]

    • nested语句中的path属性指定文档中的匹配属性名,就是设置为nested类型的属性attrs,因为attrs中是一个未知元素个数的数组,所以attrs属性被设置为nested类型,

    • 数组中的每一个元素都可能是一个json对象,每个json对象都有确定个数的多个属性名和属性值,这些要匹配的属性名和属性值在query语句中指定,注意属性名需要写完整的属性名即attrs.attrId,不要忽略前缀写attrId

    • 注意termterms都是匹配同一个字段,term是匹配属性值为单一值的记录,terms是匹配属性值为多个可能值的所有记录的并集

    • 注意使用了正确的nested查询语句的同时仍然不能使用一般语句来匹配nested类型的数据,同样不会查询到一条记录

    [响应内容]

     

nested数据聚合
  1. 聚合分析所有检索到的记录的nested类型的数据attrs

    • nested类型的数据进行聚合除了要在一般的聚合分析外面多套一层聚合分析并且用nested对象的path属性指明要被聚合分析的nested类型的字段,如下例所示

    [语法格式]

    • 第一个comments是聚合的自定义名字,age_group也是聚合的自定义名字,blogposts也是自定义聚合的名字

    1️⃣[对nested类型数据聚合查询]

    [响应结果]

     

     

 

 

 

 

附录

  1. 画图软件:Balsamiq Mockups

    • 画一下框架图很好用,看一些视频介绍还可以拿来设计网页,据说是产品原型设计软件

  2. ELK是ELasticsearch、LogStash、Kibana三个软件的首字母

    • Elasticsearch是全文检索工具

    • Kibana是页面管理工具,可以通过Kibana的管理界面操作Elasticsearch

    • LogStash是日志收集工具,通过该工具可以实现日志内容收集和格式转换

  3. Get请求也能携带请求体json

  4. 讲的太垃圾,根本没说如何设置routing为自定义路由参数,如何根据自定义的routing参数分配数据到指定分片上,并由此根据数据分片特点以特定的自定义routing参数让一次查询请求集中在一个分片上来提升查询效率

  5. 浏览器支持的请求方式不多,只有get、put、等少数几种方式,可以使用postman发起更多方式的请求,postman可以发送任何类型的Http请求,不仅可以提交表单数据,还可以提交任意类型的请求体